حزمة 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" — تأكيد بأن بادئة المفتاح تطابقه. |
baseUrl | https://api.videohati.com | أصل واجهة API. |
maxRetries | 2 | إعادة المحاولة للطلبات الآمنة وردود 429 و5xx. |
timeoutMs | 30000 | مهلة كل محاولة بالميلي ثانية. |
fetch | fetch العام | تطبيق fetch مخصّص. |
لا تُرسل مفتاح API إلى المتصفح أبدًا. يستطيع أي شخص قراءته من مصدر الصفحة واستخدام حسابك. للتشغيل، أنشئ الجلسة في الخادم وسلّم رمز الجلسة فقط إلى الصفحة.
المصادقة
يصادق العميل بمفتاح API للمشروع يُرسَل رمزًا حاملًا (bearer). استخدم مفتاح
vh_live_ في الإنتاج ومفتاح vh_test_ مقابل بيانات الاختبار — تستنتج الحزمة
الوضع من البادئة. تنشئ المفاتيح وتديرها من
لوحة التحكّم.
الفيديوهات
يغطّي المورد videos دورة الحياة كاملة: الإنشاء والرفع والقراءة وإدارة الصور
المصغّرة.
| الدالة | التوقيع | ماذا تفعل |
|---|---|---|
create | videos.create({ originalFilename, projectId? }) | تُسجّل فيديو. تُعيد { videoId, state: "uploading" }. الخطوة 1. |
list | videos.list({ limit?, cursor?, state?, projectId? }) | صفحة واحدة من الفيديوهات: { data, nextCursor? }. |
get | videos.get(videoId, { projectId? }) | التفاصيل الكاملة، بما فيها إسقاطا الرفع والترميز. |
delete | videos.delete(videoId, { projectId? }) | حذف ناعم للفيديو؛ يُلغي أي رفع نشط. |
setThumbnail | videos.setThumbnail(videoId, { data, contentType, projectId? }) | يضبط صورة مصغّرة مخصّصة (JPEG/PNG/WebP، بحدّ أقصى 5 ميغابايت). |
resetThumbnail | videos.resetThumbnail(videoId, { projectId? }) | يعود إلى إطار الملصق التلقائي. |
startUpload | videos.startUpload(videoId, { sizeBytes, contentType, checksumSha256?, idempotencyKey?, projectId? }) | يفتح رفعًا مُجزّأً؛ يُعيد حجم الجزء وعددها وقالب رابط الأجزاء. الخطوة 2. |
completeUpload | videos.completeUpload(videoId, { checksumSha256?, projectId? }) | يُنهي الرفع. الخطوة 3. |
abortUpload | videos.abortUpload(videoId, { projectId? }) | يُلغي رفعًا مفتوحًا. |
upload | videos.upload(file, options?) | يُنفّذ إنشاء ← بدء ← رفع الأجزاء ← إكمال في استدعاء واحد. |
waitForState | videos.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. |
partConcurrency | 4 | عدد الأجزاء المرفوعة بالتوازي. |
maxPartAttempts | 3 | عدد المحاولات لكل جزء. |
videoId | — | إعادة استخدام فيديو منشأ مسبقًا بدل إنشاء واحد. |
signal | — | AbortSignal لإلغاء الرفع. |
fetch | fetch العام | 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).
| الدالة | التوقيع | ماذا تفعل |
|---|---|---|
createSession | playback.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.
| الدالة | التوقيع | ماذا تفعل |
|---|---|---|
register | webhooks.register(projectId, { url, description? }) | تُسجّل نقطة نهاية. يُعاد secret مرة واحدة. |
list | webhooks.list(projectId) | تسرد نقاط النهاية. |
get | webhooks.get(projectId, id) | نقطة نهاية واحدة. |
update | webhooks.update(projectId, id, { url?, description?, enabled? }) | تُحدّث نقطة نهاية. |
delete | webhooks.delete(projectId, id) | حذف ناعم لنقطة نهاية. |
rotateSecret | webhooks.rotateSecret(projectId, id) | تسكّ سرًّا جديدًا (يُعاد مرة واحدة). |
reopenCircuit | webhooks.reopenCircuit(projectId, id) | تُعيد نقطة نهاية متعثّرة إلى الخدمة. |
sendTest | webhooks.sendTest(projectId, id) | تُدرِج تسليمًا اصطناعيًا من نوع video.encoding.ready. |
listDeliveries | webhooks.listDeliveries(projectId, id, { limit?, cursor?, state?, eventType? }) | صفحة واحدة من محاولات التسليم. |
verify | webhooks.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. فئة فرعية من خطأ الاتصال. |
VideohatiValidationError | 400 | فشل الجسم أو الاستعلام في التحقّق. |
VideohatiAuthenticationError | 401 | اعتماد مفقود أو غير صالح. |
VideohatiPermissionError | 403 | اعتماد صالح، لكن الصلاحية غير كافية. |
VideohatiNotFoundError | 404 | المورد مفقود أو غير مرئيّ لهذا المستدعي. |
VideohatiConflictError | 409 | تعارض مع الحالة الراهنة للمورد. |
VideohatiRateLimitError | 429 | تجاوز حدّ المعدّل؛ اقرأ retryAfterSeconds. |
VideohatiServerError | 5xx | فشلت واجهة 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.