حزمة React
حزمة @videohati/react الرسمية — مكوّن VideohatiPlayer مع خطّافات الرفع والبيانات الوصفية وتسليمات Webhook لـ React 18+ وNext.js.
تمنحك @videohati/react مكوّن <VideohatiPlayer> الذي يغلّف
@videohati/player، مع خطّافات لرفع المتصفح والبيانات
الوصفية للفيديو وتسليمات Webhook. الإصدار 1.0.0-rc.0.
التثبيت
React الإصدار 18 فأحدث. المشغّل تبعية نظير، فثبّت الاثنين.
npm install @videohati/react @videohati/playerالمزوّد
غلّف تطبيقك مرة واحدة. يشارك VideohatiProvider أصل واجهة API — ومفتاح API
اختياريًا للنماذج الأولية — مع الخطّافات.
| الخاصية | الافتراضي | ماذا تفعل |
|---|---|---|
baseUrl | https://api.videohati.com | أصل واجهة API. |
apiKey | — | مفتاح API اختياري لطلبات المتصفح. غير مستحسَن. |
children | — | تطبيقك. |
import { VideohatiProvider } from "@videohati/react";
<VideohatiProvider baseUrl="https://api.videohati.com">
<App />
</VideohatiProvider>;تُعيد useVideohatiConfig() القيمة الحالية { baseUrl, apiKey? } عند الحاجة.
لا تُرسل مفتاح API حيًّا إلى المتصفح أبدًا. يستخدم التشغيل رموز جلسات مُنشأة في
الخادم. خاصية apiKey للنماذج الأولية بمفاتيح vh_test_ فقط.
تشغيل فيديو
يأتي sessionToken من POST /v1/playback/sessions في الخادم — استخدم
@videohati/node في معالِج المسار أو في إجراء الخادم.
"use client";
import { VideohatiPlayer } from "@videohati/react";
export function Lesson({ sessionToken }: { sessionToken: string }) {
return (
<VideohatiPlayer
videoId="01JZ9WV3N8GQ5T2M7K4C6XBARH"
projectId="01JZ9WV3N8GQ5T2M7K4C6XBARF"
sessionToken={sessionToken}
autoplay="muted"
lang="ar"
width={640}
height={360}
onStateChange={(state) => console.log(state)}
/>
);
}خصائص VideohatiPlayerProps:
| الخاصية | النوع | ماذا تفعل |
|---|---|---|
videoId | string (مطلوب) | الفيديو المراد تشغيله. |
projectId | string (مطلوب) | المشروع المالك للفيديو. |
sessionToken | string (مطلوب) | رمز من جلسة تشغيل مُنشأة في الخادم. |
autoplay | boolean | "muted" | "muted" يشغّل تلقائيًا مكتومًا؛ true يحاول بلا كتم؛ الافتراضي false. |
width | number | عرض المشغّل بالبكسل. |
height | number | ارتفاع المشغّل بالبكسل. |
lang | "ar" | "en" | لغة الواجهة. الافتراضي العربية. |
viewerText | string | نص العلامة المائية؛ يعود إلى نص المشاهد في الجلسة. |
apiOrigin | string | تجاوز أصل واجهة API (بيئة التجهيز، تجاوزات محلية). |
className | string | صنف على عنصر الحاوية. |
style | CSSProperties | نمط مضمّن على الحاوية. |
onStateChange | (state) => void | يُطلَق عند كل تغيّر في حالة التشغيل. |
onError | ({ code, message }) => void | يُطلَق عند خطأ في التشغيل. |
onTimeUpdate | ({ currentTime, duration }) => void | يُطلَق مع حركة مؤشّر التشغيل. |
playerRef | (player | null) => void | يستقبل كائن المشغّل الأساسي — استدعِ عليه play/seek/setQuality. |
الرفع من المتصفح
useVideohatiUpload({ projectId?, partConcurrency?, idempotencyKey? }) يُنفّذ
تدفّق الرفع المُجزّأ نفسه في حزمة Node (إنشاء ← بدء ← رفع الأجزاء ← إكمال)،
بثلاثة أجزاء متوازية افتراضيًا.
الخيارات:
| الخيار | الافتراضي | ماذا يفعل |
|---|---|---|
projectId | — | مطلوب مع مصادقة كعكة الجلسة؛ ويُتجاهَل مع مصادقة مفتاح API. |
partConcurrency | 3 | عدد الأجزاء المرفوعة بالتوازي (ميزانية اتصالات المتصفح). |
idempotencyKey | — | هوية ثابتة لهذه المحاولة؛ تجعل إعادة المحاولة تُعيد الفيديو نفسه وجلسة الرفع نفسها. انظر أدناه. |
يُعيد:
| الحقل | النوع | ما هو |
|---|---|---|
upload | (file: File) => Promise<UploadResult | null> | يبدأ الرفع. |
progress | UploadProgress | null | عدّاد حيّ للبايتات والأجزاء. |
state | "idle" | "creating" | "uploading" | "completing" | "done" | "error" | المرحلة الحالية. |
error | { status, code, message } | null | الإخفاق، إن وُجد. |
video | UploadResult | null | الفيديو المكتمل، حين تصبح state هي done. |
reset | () => void | يُعيد الخطّاف إلى idle (يُتجاهَل أثناء الانشغال). |
"use client";
import { useVideohatiUpload } from "@videohati/react";
function Uploader({ projectId }: { projectId: string }) {
const { upload, progress, state, error, video, reset } = useVideohatiUpload({
projectId,
});
return (
<form onSubmit={(e) => e.preventDefault()}>
<input
type="file"
accept="video/*"
disabled={state === "creating" || state === "uploading" || state === "completing"}
onChange={(e) => e.target.files?.[0] && upload(e.target.files[0])}
/>
{progress && (
<progress value={progress.uploadedBytes} max={progress.totalBytes} />
)}
{state === "done" && <p>تم الرفع: {video?.videoId}</p>}
{state === "error" && <p role="alert">{error?.message}</p>}
{(state === "done" || state === "error") && (
<button type="button" onClick={reset}>
رفع فيديو آخر
</button>
)}
</form>
);
}إعادة محاولة الرفع بأمان
مرّر idempotencyKey فتصبح الكتابتان اللتان تُنشئان حالة على الخادم — الفيديو
وجلسة الرفع — آمنتين للتكرار. استدعِ upload مرة أخرى بعد إخفاق فتحصل على
معرّف الفيديو نفسه، حتى لو ضاعت استجابة المحاولة الأولى في الطريق بعد أن
أثبتها الخادم فعلًا. وبغير مفتاح تصبح تلك الاستجابة الضائعة لا تُميَّز عن طلب لم
يصل قطّ، فتُنشئ إعادة المحاولة فيديو ثانيًا.
وفي المتصفح يكون ما تتعافى منه عادةً إعادة تحميل الصفحة، وإعادة التحميل تمحو كل
ما يحمله الخطّاف — فلا بدّ أن يأتي المفتاح من موضع يبقى بعدها. اقرأه من معرّف
سجلّ تملكه أصلًا، أو اكتبه في localStorage قبل المحاولة الأولى. أمّا قيمة من
Math.random() أو Date.now() أو قيمة تُولَّد من جديد في كل عرض فلا تمنحك حماية
إطلاقًا، لأن إعادة المحاولة تُرسل مفتاحًا مختلفًا.
قيدان آخران:
- لا شيء من نقل البايتات يُستأنف. إعادة المحاولة يُعيد إرسال كل الأجزاء من البداية. الضمان يخصّ عدم تكرار الفيديو.
- المفتاح الواحد لملف واحد. يربط الخطّاف المفتاح باسم الملف وحجمه، فإعادة استخدام مفتاح لملف مختلف تبدأ رفعًا جديدًا فعليًا بدل إعادة فيديو الملف الأول. ومع ذلك، فضّل مفتاحًا جديدًا لكل ملف.
يحتاج اشتقاق المفتاح إلى crypto.subtle، ولا تكشفه المتصفحات إلا في سياق آمن
(HTTPS أو localhost). وحين يغيب يفشل الرفع بـ
error.code === "idempotency_unavailable" بدل المضيّ بهدوء بلا الحماية التي
طلبتها.
function ResumableUploader({ projectId, jobId }: { projectId: string; jobId: string }) {
// يأتي jobId من تخزينك، فتُعيد إعادة التحميل استخدام المفتاح نفسه.
const { upload, state, error } = useVideohatiUpload({
projectId,
idempotencyKey: jobId,
});
return (
<>
<input type="file" onChange={(e) => e.target.files?.[0] && upload(e.target.files[0])} />
{state === "error" && <p role="alert">{error?.message}</p>}
</>
);
}قراءة البيانات الوصفية للفيديو
useVideohatiVideo(videoId, { projectId?, pollIntervalMs? }) يقرأ فيديو ويُبقيه
محدّثًا.
import { useVideohatiVideo } from "@videohati/react";
const { video, error, isLoading, refresh } = useVideohatiVideo(videoId, {
projectId,
});يُخزَّن عبر عمليات التركيب، ويُعاد التحقّق منه عند التركيب، ويُستقصى كل 5 ثوانٍ
ما دامت video.state هي uploading أو uploaded أو encoding — فيتحدّث الفيديو
قيد المعالجة تلقائيًا. اضبط pollIntervalMs: 0 لتعطيل الاستقصاء. وتُعيد
clearVideohatiVideoCache() ضبط الذاكرة المؤقتة المشتركة.
تسليمات Webhook
useWebhookDeliveries(projectId, endpointId, { state?, eventType?, limit? })
يقرأ صفحة واحدة من محاولات التسليم لنقطة نهاية Webhook.
import { useWebhookDeliveries } from "@videohati/react";
const { deliveries, nextCursor, error, isLoading, refresh } = useWebhookDeliveries(
projectId,
endpointId,
{ limit: 20 },
);بنفس التخزين المؤقت لـ useVideohatiVideo. وتُعيد clearWebhookDeliveriesCache()
ضبط ذاكرته المؤقتة.
موجّه Next.js App Router
المشغّل وكل خطّاف مخصّصان للعميل فقط — ضعها في مكوّن يبدأ بـ "use client". سُكّ
رمز الجلسة في الخادم (معالِج مسار أو إجراء خادم) بـ @videohati/node، ثم مرّره
خاصيةً. النمط الكامل في دليل التضمين مع Next.js.
معالجة الأخطاء
تكشف الخطّافات الأخطاء بصيغة { status, code, message } — وcode رمز الآلة من
واجهة API (مثل account_not_approved وvideo_not_found). ويُبلّغ المشغّل عن
أخطاء التشغيل عبر onError.
التحقّق من Webhooks
يتمّ التحقّق من توقيع Webhook في الخادم — استخدم verifyWebhookSignature من
@videohati/node في معالِج المسار. انظر دليل Webhooks.