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

حزمة 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 اختياريًا للنماذج الأولية — مع الخطّافات.

الخاصيةالافتراضيماذا تفعل
baseUrlhttps://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:

الخاصيةالنوعماذا تفعل
videoIdstring (مطلوب)الفيديو المراد تشغيله.
projectIdstring (مطلوب)المشروع المالك للفيديو.
sessionTokenstring (مطلوب)رمز من جلسة تشغيل مُنشأة في الخادم.
autoplayboolean | "muted""muted" يشغّل تلقائيًا مكتومًا؛ true يحاول بلا كتم؛ الافتراضي false.
widthnumberعرض المشغّل بالبكسل.
heightnumberارتفاع المشغّل بالبكسل.
lang"ar" | "en"لغة الواجهة. الافتراضي العربية.
viewerTextstringنص العلامة المائية؛ يعود إلى نص المشاهد في الجلسة.
apiOriginstringتجاوز أصل واجهة API (بيئة التجهيز، تجاوزات محلية).
classNamestringصنف على عنصر الحاوية.
styleCSSPropertiesنمط مضمّن على الحاوية.
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.
partConcurrency3عدد الأجزاء المرفوعة بالتوازي (ميزانية اتصالات المتصفح).
idempotencyKeyهوية ثابتة لهذه المحاولة؛ تجعل إعادة المحاولة تُعيد الفيديو نفسه وجلسة الرفع نفسها. انظر أدناه.

يُعيد:

الحقلالنوعما هو
upload(file: File) => Promise<UploadResult | null>يبدأ الرفع.
progressUploadProgress | nullعدّاد حيّ للبايتات والأجزاء.
state"idle" | "creating" | "uploading" | "completing" | "done" | "error"المرحلة الحالية.
error{ status, code, message } | nullالإخفاق، إن وُجد.
videoUploadResult | 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.