تنفيذ عمليات فعالة لكل إطار فيديو في الفيديو باستخدام الدالة requestVideoFrameCallback()

تعرَّف على كيفية استخدام requestVideoFrameCallback() لزيادة كفاءة العمل مع الفيديوهات في المتصفّح.

تسمح طريقة HTMLVideoElement.requestVideoFrameCallback() لمطوّري الويب بتسجيل دالة ردّ الاتصال التي يتم تنفيذها في خطوات العرض عند إرسال إطار فيديو جديد إلى برنامج التركيب. يتيح ذلك للمطوّرين إجراء عمليات فعّالة على مستوى كل إطار فيديو، مثل معالجة الفيديو والرسم على لوحة العرض أو تحليل الفيديو أو المزامنة مع مصادر الصوت الخارجية.

الاختلاف عن requestAnimationFrame()

سيتم مزامنة العمليات، مثل رسم إطار فيديو على لوحة عرض باستخدام drawImage() التي يتم إجراؤها من خلال واجهة برمجة التطبيقات هذه، بأفضل جهد ممكن مع معدّل عرض الفيديو الذي يتم تشغيله على الشاشة. يختلف requestVideoFrameCallback() عن window.requestAnimationFrame()، الذي يتم تنشيطه عادةً 60 مرة في الثانية، لأنّ requestVideoFrameCallback() مرتبط بمعدّل عرض اللقطات الفعلي للفيديو، مع استثناء مهم:

معدّل تنفيذ الدوالّ البرمجية هو المعدّل الأقل بين معدّل الفيديو ومعدّل المتصفّح. وهذا يعني أنّ فيديو بمعدّل 25 لقطة في الثانية يتم تشغيله في متصفّح يعرض بمعدّل 60 هرتز سيؤدي إلى تشغيل عمليات رد الاتصال بمعدّل 25 هرتز. سيتم تشغيل فيديو بمعدل 120 لقطة في الثانية في المتصفّح نفسه الذي يعمل بمعدل 60 هرتز، وسيتم إطلاق عمليات رد الاتصال بمعدل 60 هرتز.

ماذا يجب أن يتضمّن الاسم؟

بسبب تشابهها مع window.requestAnimationFrame()، تم في البداية اقتراح الطريقة باسم video.requestAnimationFrame() وتمت إعادة تسميتها إلى requestVideoFrameCallback()، وتم الاتفاق على ذلك بعد مناقشة مطوّلة.

رصد الميزات

if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
  // The API is supported!
}

دعم المتصفح

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: 132.
  • Safari: 15.4.

Source

Polyfill

يتوفّر polyfill لطريقة requestVideoFrameCallback() استنادًا إلى Window.requestAnimationFrame() وHTMLVideoElement.getVideoPlaybackQuality(). قبل استخدام هذه الميزة، يُرجى التعرّف على القيود المذكورة في README.

استخدام طريقة requestVideoFrameCallback()

إذا سبق لك استخدام الطريقة requestAnimationFrame()، ستشعر على الفور بالاعتياد على الطريقة requestVideoFrameCallback(). عليك تسجيل دالة معالجة أولية مرة واحدة، ثم إعادة تسجيلها كلما تم تشغيلها.

const doSomethingWithTheFrame = (now, metadata) => {
  // Do something with the frame.
  console.log(now, metadata);
  // Re-register the callback to be notified about the next frame.
  video.requestVideoFrameCallback(doSomethingWithTheFrame);
};
// Initially register the callback to be notified about the first frame.
video.requestVideoFrameCallback(doSomethingWithTheFrame);

في دالة ردّ الاتصال، now هي DOMHighResTimeStamp وmetadata هي قاموس VideoFrameMetadata يتضمّن السمات التالية:

  • presentationTime، من النوع DOMHighResTimeStamp: الوقت الذي أرسل فيه وكيل المستخدم الإطار لتركيبه.
  • expectedDisplayTime، من النوع DOMHighResTimeStamp: الوقت الذي يتوقّع فيه وكيل المستخدم أن يكون الإطار مرئيًا.
  • width، من النوع unsigned long: تمثّل عرض إطار الفيديو بوحدات بكسل الوسائط.
  • height، من النوع unsigned long: تمثّل هذه السمة ارتفاع إطار الفيديو بالبكسل.
  • mediaTime، من النوع double: الطابع الزمني لعرض الوسائط (PTS) بالثواني للإطار المعروض (على سبيل المثال، الطابع الزمني على المخطط الزمني video.currentTime).
  • presentedFrames، من النوع unsigned long: عدد اللقطات التي تم إرسالها لإنشاء الفيديو. يسمح هذا الحقل للعملاء بتحديد ما إذا تم تفويت أي إطارات بين مثيلات VideoFrameRequestCallback.
  • processingDuration، من النوع double: المدة المنقضية بالثواني منذ إرسال الحزمة المرمّزة التي تحمل الطابع الزمني نفسه للعرض التقديمي (PTS) مثل هذا الإطار (على سبيل المثال، مثل mediaTime) إلى برنامج الترميز إلى أن أصبح الإطار الذي تم فك ترميزه جاهزًا للعرض.

بالنسبة إلى تطبيقات WebRTC، قد تظهر خصائص إضافية:

  • captureTime، من النوع DOMHighResTimeStamp: بالنسبة إلى لقطات الفيديو الواردة من مصدر محلي أو بعيد، هذا هو الوقت الذي التقطت فيه الكاميرا اللقطة. بالنسبة إلى مصدر بعيد، يتم تقدير وقت الالتقاط باستخدام مزامنة الساعة وتقارير المرسل RTCP لتحويل الطوابع الزمنية لبروتوكول RTP إلى وقت الالتقاط.
  • receiveTime، من النوع DOMHighResTimeStamp: بالنسبة إلى لقطات الفيديو الواردة من مصدر بعيد، هذا هو الوقت الذي تلقّت فيه المنصة اللقطة المرمّزة، أي الوقت الذي تم فيه تلقّي آخر حزمة تنتمي إلى هذه اللقطة عبر الشبكة.
  • rtpTimestamp، من النوع unsigned long: الطابع الزمني لبروتوكول النقل في الوقت الفعلي (RTP) المرتبط بإطار الفيديو هذا.

من العناصر المهمة في هذه القائمة mediaTime. يستخدم Chromium ساعة الصوت كمصدر الوقت الذي يستند إليه video.currentTime، بينما يتم ملء mediaTime مباشرةً بواسطة presentationTimestamp للإطار. يجب استخدام mediaTime إذا أردت تحديد اللقطات بدقة وبطريقة قابلة للتكرار، بما في ذلك تحديد اللقطات التي فاتتك بالضبط.

إذا بدا أنّ هناك اختلافًا بمقدار إطار واحد…

المزامنة العمودية (أو vsync فقط) هي تقنية رسومات تعمل على مزامنة عدد اللقطات في الثانية للفيديو مع معدل إعادة التحميل للشاشة. بما أنّ requestVideoFrameCallback() يتم تشغيله على سلسلة التعليمات الرئيسية، ولكن تتم عملية تركيب الفيديو بشكل غير مرئي على سلسلة تعليمات التركيب، فإنّ كل ما تقدّمه واجهة برمجة التطبيقات هذه هو أفضل ما يمكن تقديمه، ولا يقدّم المتصفّح أي ضمانات صارمة. قد يكون السبب هو أنّ واجهة برمجة التطبيقات يمكن أن تتأخر بمقدار مزامنة عمودية واحدة مقارنةً بوقت عرض إطار الفيديو. يستغرق ظهور التغييرات التي يتم إجراؤها على صفحة الويب من خلال واجهة برمجة التطبيقات على الشاشة دورة مزامنة عمودية واحدة (كما هو الحال مع window.requestAnimationFrame()). لذلك، إذا واصلت تعديل mediaTime أو رقم الإطار على صفحة الويب وقارنت ذلك بإطارات الفيديو المرقمة، سيبدو الفيديو في النهاية وكأنّه يسبق الإطار بمقدار إطار واحد.

ما يحدث فعلاً هو أنّ اللقطة تكون جاهزة عند مزامنة الإشارات العمودية x، ويتم تشغيل دالة الرجوع وعرض اللقطة عند مزامنة الإشارات العمودية x+1، ويتم عرض التغييرات التي تم إجراؤها في دالة الرجوع عند مزامنة الإشارات العمودية x+2. يمكنك التحقّق مما إذا كان رد الاتصال متأخرًا عن مزامنة الإشارات العمودية (وتم عرض اللقطة على الشاشة) من خلال التحقّق مما إذا كان metadata.expectedDisplayTime يساوي تقريبًا now أو مزامنة إشارات عمودية واحدة في المستقبل. إذا كان الوقت ضمن نطاق خمسة إلى عشرة ميكروثانية من now، يكون الإطار معروضًا حاليًا؛ أما إذا كان expectedDisplayTime بعد حوالي ستة عشر ملي ثانية (بافتراض أنّ المتصفح/الشاشة يعيدان التحميل بمعدل 60 هرتز)، فيكون الوقت متزامنًا مع الإطار.

عرض توضيحي

لقد أنشأت عرضًا توضيحيًا صغيرًا يوضّح كيفية رسم اللقطات على لوحة عرض بمعدّل اللقطات في الثانية للفيديو بالضبط، وكيفية تسجيل بيانات تعريف اللقطات لأغراض تصحيح الأخطاء.

let paintCount = 0;
let startTime = 0.0;

const updateCanvas = (now, metadata) => {
  if (startTime === 0.0) {
    startTime = now;
  }

  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

  const elapsed = (now - startTime) / 1000.0;
  const fps = (++paintCount / elapsed).toFixed(3);
  fpsInfo.innerText = `video fps: ${fps}`;
  metadataInfo.innerText = JSON.stringify(metadata, null, 2);

  video.requestVideoFrameCallback(updateCanvas);
};

video.requestVideoFrameCallback(updateCanvas);

الاستنتاجات

لقد كان بإمكان المستخدمين معالجة الفيديوهات على مستوى الإطارات لفترة طويلة، ولكن بدون الوصول إلى الإطارات الفعلية، بل استنادًا إلى video.currentTime فقط. تُحسّن طريقة requestVideoFrameCallback() هذا الحلّ البديل بشكل كبير.

الإقرارات

تم تحديد مواصفات واجهة برمجة التطبيقات requestVideoFrameCallback وتنفيذها بواسطة توماس غيلبرت. راجع هذه المشاركة جو ميدلي وكايس باسكس. الصورة الرئيسية من Denise Jans على Unsplash.