فيديوهاتيالتوثيق

حزمة Node.js

حزمة ‎@videohati/node‎ الرسمية — إنشاء عميل ورفع الفيديو وإنشاء جلسات التشغيل وإدارة Webhooks ومعالجة الأخطاء من خادم Node.js.

@videohati/node هي حزمة الخادم الرسمية. وهي بصيغة ESM، بلا تبعيات وقت التشغيل، ومكتوبة بأنواع مطابقة لمواصفة OpenAPI 3.1 المنشورة على ‏https://api.videohati.com/openapi.json. الإصدار 1.0.0-rc.0.

التثبيت

‏Node.js الإصدار 18 فأحدث. الحزمة بصيغة ESM فقط — استخدم import لا require.

npm install @videohati/node

إنشاء عميل

أنشئ عميلًا واحدًا وأعد استخدامه. يتبع الوضع (live أو test) بادئة المفتاح دائمًا — فمفتاح vh_test_ يعمل مقابل بيانات الاختبار.

import { Videohati } from "@videohati/node";

const videohati = new Videohati({
  apiKey: process.env.VIDEOHATI_API_KEY, // vh_live_... أو vh_test_...
});

خيارات VideohatiOptions:

الخيارالافتراضيماذا يفعل
apiKeyمفتاح API للمشروع (vh_live_... أو vh_test_...).
env"live" | "test" — تأكيد بأن بادئة المفتاح تطابقه.
baseUrlhttps://api.videohati.comأصل واجهة API.
maxRetries2إعادة المحاولة للطلبات الآمنة وردود 429 و5xx.
timeoutMs30000مهلة كل محاولة بالميلي ثانية.
fetchfetch العامتطبيق fetch مخصّص.

لا تُرسل مفتاح API إلى المتصفح أبدًا. يستطيع أي شخص قراءته من مصدر الصفحة واستخدام حسابك. للتشغيل، أنشئ الجلسة في الخادم وسلّم رمز الجلسة فقط إلى الصفحة.

المصادقة

يصادق العميل بمفتاح API للمشروع يُرسَل رمزًا حاملًا (bearer). استخدم مفتاح ‏vh_live_ في الإنتاج ومفتاح vh_test_ مقابل بيانات الاختبار — تستنتج الحزمة الوضع من البادئة. تنشئ المفاتيح وتديرها من لوحة التحكّم.

الفيديوهات

يغطّي المورد videos دورة الحياة كاملة: الإنشاء والرفع والقراءة وإدارة الصور المصغّرة.

الدالةالتوقيعماذا تفعل
createvideos.create({ originalFilename, projectId? })تُسجّل فيديو. تُعيد { videoId, state: "uploading" }. الخطوة 1.
listvideos.list({ limit?, cursor?, state?, projectId? })صفحة واحدة من الفيديوهات: { data, nextCursor? }.
getvideos.get(videoId, { projectId? })التفاصيل الكاملة، بما فيها إسقاطا الرفع والترميز.
deletevideos.delete(videoId, { projectId? })حذف ناعم للفيديو؛ يُلغي أي رفع نشط.
setThumbnailvideos.setThumbnail(videoId, { data, contentType, projectId? })يضبط صورة مصغّرة مخصّصة (JPEG/PNG/WebP، بحدّ أقصى 5 ميغابايت).
resetThumbnailvideos.resetThumbnail(videoId, { projectId? })يعود إلى إطار الملصق التلقائي.
startUploadvideos.startUpload(videoId, { sizeBytes, contentType, checksumSha256?, idempotencyKey?, projectId? })يفتح رفعًا مُجزّأً؛ يُعيد حجم الجزء وعددها وقالب رابط الأجزاء. الخطوة 2.
completeUploadvideos.completeUpload(videoId, { checksumSha256?, projectId? })يُنهي الرفع. الخطوة 3.
abortUploadvideos.abortUpload(videoId, { projectId? })يُلغي رفعًا مفتوحًا.
uploadvideos.upload(file, options?)يُنفّذ إنشاء ← بدء ← رفع الأجزاء ← إكمال في استدعاء واحد.
waitForStatevideos.waitForState(videoId, { targetStates?, intervalMs?, timeoutMs?, projectId? })يتابع الحالة حتى يبلغ الفيديو حالة هدفًا.

الرفع في استدعاء واحد

videos.upload(file, options) يُنفّذ تدفّق الرفع المُجزّأ كاملًا في استدعاء واحد. وfile مسار ملف أو Buffer/Uint8Array أو Blob/File.

const uploaded = await videohati.videos.upload("./lecture-01.mp4", {
  onProgress: (p) => console.log(`${p.uploadedBytes}/${p.totalBytes} bytes`),
});

// استقصِ الحالة حتى يصبح الفيديو قابلًا للتشغيل من طرف إلى طرف:
const video = await videohati.videos.waitForState(uploaded.videoId);
console.log(video.state); // "ready"

خيارات UploadOptions:

الخيارالافتراضيماذا يفعل
projectIdيُتجاهَل مع مصادقة مفتاح API (المفتاح مقيَّد بمشروع أصلًا).
filenameاسم الملف، وإلا upload.binالاسم الأصلي المخزَّن للملف.
contentTypeيُستنتَج من الامتدادنوع MIME المُرسَل إلى الخادم.
checksumSha256تجزئة SHA-256 بالنظام الستّ عشري الصغير للملف كاملًا؛ يتحقّق منها الخادم.
idempotencyKeyيجعل الرفع قابلًا للاستئناف بعد إعادة التشغيل.
onProgress(progress) => void بـ uploadedBytes وtotalBytes وpartsCompleted وtotalParts.
partConcurrency4عدد الأجزاء المرفوعة بالتوازي.
maxPartAttempts3عدد المحاولات لكل جزء.
videoIdإعادة استخدام فيديو منشأ مسبقًا بدل إنشاء واحد.
signalAbortSignal لإلغاء الرفع.
fetchfetch العامfetch مخصّص لرفع الأجزاء.

يفترض waitForState القيم targetStates: ["ready", "failed"] وintervalMs: 3000 وtimeoutMs: 900000 (15 دقيقة)؛ ويُطلق VideohatiError عند انتهاء المهلة.

القراءة والحذف

const { data } = await videohati.videos.list({ state: "ready" });

const detail = await videohati.videos.get(videoId);
console.log(detail.state); // uploading | uploaded | encoding | ready | failed

await videohati.videos.delete(videoId); // حذف ناعم؛ يُلغي أي رفع نشط

التشغيل

أنشئ الجلسة في خادمك وسلّم sessionToken فقط إلى الصفحة (ضمّنها مع ‏@videohati/player أو @videohati/react).

الدالةالتوقيعماذا تفعل
createSessionplayback.createSession({ videoId, ttlSeconds?, referrerAllowlist?, viewerIdFromRequest?, viewerDisplayText?, projectId? })تُنشئ جلسة في الخادم؛ تُعيد sessionToken للمشغّل.

يستدعي تطبيقك createSession فقط؛ أمّا بقية نداءات التشغيل فيتولّاها المشغّل بنفسه عبر رمز الجلسة.

معاملات createSession:

المعاملالافتراضيماذا يفعل
videoIdمطلوبالفيديو المراد تشغيله.
ttlSecondsافتراضي الخادمعمر الجلسة.
referrerAllowlistحصر التشغيل في هذه المُحيلات.
viewerIdFromRequestمعرّف مبهم للمشاهد للتحليلات والتتبّع الجنائي.
viewerDisplayTextالنص المرسوم في العلامة المائية على الفيديو.
projectIdيُتجاهَل مع مصادقة مفتاح API (المفتاح مقيَّد بمشروع أصلًا).
const session = await videohati.playback.createSession({
  videoId,
  viewerDisplayText: "أحمد ك.",
});
console.log(session.sessionToken);

Webhooks

سجّل نقاط النهاية، وافحص التسليمات، وتحقّق من التواقيع. التدفّق الكامل في دليل Webhooks.

الدالةالتوقيعماذا تفعل
registerwebhooks.register(projectId, { url, description? })تُسجّل نقطة نهاية. يُعاد secret مرة واحدة.
listwebhooks.list(projectId)تسرد نقاط النهاية.
getwebhooks.get(projectId, id)نقطة نهاية واحدة.
updatewebhooks.update(projectId, id, { url?, description?, enabled? })تُحدّث نقطة نهاية.
deletewebhooks.delete(projectId, id)حذف ناعم لنقطة نهاية.
rotateSecretwebhooks.rotateSecret(projectId, id)تسكّ سرًّا جديدًا (يُعاد مرة واحدة).
reopenCircuitwebhooks.reopenCircuit(projectId, id)تُعيد نقطة نهاية متعثّرة إلى الخدمة.
sendTestwebhooks.sendTest(projectId, id)تُدرِج تسليمًا اصطناعيًا من نوع video.encoding.ready.
listDeliverieswebhooks.listDeliveries(projectId, id, { limit?, cursor?, state?, eventType? })صفحة واحدة من محاولات التسليم.
verifywebhooks.verify({ payload, header, secret, toleranceSeconds? })تتحقّق من توقيع وتُعيد الغلاف المُحلَّل.

التحقّق من توقيع

يُصدَّر verifyWebhookSignature أيضًا مستقلًّا. مرّر جسم الطلب الخام — لا JSON مُعاد تسلسله.

import { verifyWebhookSignature, SIGNATURE_HEADER } from "@videohati/node";

app.post("/webhooks/videohati", (req, res) => {
  const { valid, event } = verifyWebhookSignature({
    payload: req.rawBody, // نص أو Uint8Array، كما وصل تمامًا
    header: req.headers[SIGNATURE_HEADER], // "videohati-signature"
    secret: process.env.VIDEOHATI_WEBHOOK_SECRET,
  });
  if (!valid) return res.status(400).end();
  if (event?.type === "video.encoding.ready") {
    // الفيديو جاهز — أنشئ جلسة تشغيل، أبلغ المستخدم، إلخ.
  }
  res.status(200).end();
});

الترويسة هي Videohati-Signature: t=<unix_ts>,v1=<hex_sha256> (HMAC-SHA256 على ‏<unix_ts>.<raw_body>)؛ والتحقّق ثابت الزمن ويرفض الطوابع الزمنية الأقدم من 300 ثانية (اضبطها بـ toleranceSeconds).

المشاريع ومفاتيح API

تنشئ المشاريع ومفاتيح API وتديرها من لوحة التحكّم، لا عبر الحزمة. انظر المفاهيم لمعرفة العلاقة بين الحسابات والمشاريع والمفاتيح.

الأخطاء

كل استجابة غير 2xx تُطلق فئة فرعية مكتوبة من VideohatiError، تحمل status وcode (رمز الآلة من واجهة API، مثل video_not_found). لا تحتوي الأخطاء على مفتاح API أبدًا.

الفئةالحالةمتى
VideohatiConnectionErrorلم يصل الطلب إلى واجهة API (DNS، TLS، المقبس).
VideohatiTimeoutErrorأُلغي الطلب بعد timeoutMs. فئة فرعية من خطأ الاتصال.
VideohatiValidationError400فشل الجسم أو الاستعلام في التحقّق.
VideohatiAuthenticationError401اعتماد مفقود أو غير صالح.
VideohatiPermissionError403اعتماد صالح، لكن الصلاحية غير كافية.
VideohatiNotFoundError404المورد مفقود أو غير مرئيّ لهذا المستدعي.
VideohatiConflictError409تعارض مع الحالة الراهنة للمورد.
VideohatiRateLimitError429تجاوز حدّ المعدّل؛ اقرأ retryAfterSeconds.
VideohatiServerError5xxفشلت واجهة API في إتمام الطلب.
import {
  VideohatiError,
  VideohatiNotFoundError,
  VideohatiRateLimitError,
} from "@videohati/node";

try {
  await videohati.videos.get(videoId);
} catch (error) {
  if (error instanceof VideohatiNotFoundError) {
    // 404 — error.code === "video_not_found"
  } else if (error instanceof VideohatiRateLimitError) {
    await sleep((error.retryAfterSeconds ?? 1) * 1000);
  } else if (error instanceof VideohatiError) {
    console.error(error.status, error.code, error.message);
  }
}

تُعاد محاولة طلبات GET وDELETE وردود 429 و5xx بتراجع أُسّي محدود بـ 8 ثوانٍ — محاولتان افتراضيًا. ويحترم ردّ 429 ترويسة Retry-After. اضبط السلوك بـ ‏maxRetries وtimeoutMs.