انجام عملیات کارآمد به ازای هر فریم ویدیو

یاد بگیرید که چگونه از requestVideoFrameCallback() برای کار موثرتر با ویدیوها در مرورگر استفاده کنید.

منتشر شده: ۸ ژانویه ۲۰۲۳

Browser Support

  • کروم: ۸۳.
  • لبه: ۸۳.
  • فایرفاکس: ۱۳۲.
  • سافاری: ۱۵.۴.

Source

متد HTMLVideoElement.requestVideoFrameCallback() به نویسندگان وب اجازه می‌دهد تا یک فراخوانی (callback) ثبت کنند که در مراحل رندرینگ، زمانی که یک فریم ویدیویی جدید به کامپوزیتور ارسال می‌شود، اجرا می‌شود. این به توسعه‌دهندگان اجازه می‌دهد تا عملیات کارآمدی را برای هر فریم ویدیو روی ویدیو انجام دهند، مانند پردازش ویدیو و نقاشی روی بوم، تجزیه و تحلیل ویدیو یا هماهنگ‌سازی با منابع صوتی خارجی.

تفاوت با requestAnimationFrame()

عملیاتی که با این API انجام می‌شوند، مانند ترسیم یک فریم ویدیویی روی یک بوم با drawImage() ، به بهترین شکل با نرخ فریم ویدیوی در حال پخش روی صفحه هماهنگ می‌شوند. این با تابع window.requestAnimationFrame() که معمولاً حدود ۶۰ بار در ثانیه اجرا می‌شود، متفاوت است.

requestVideoFrameCallback() به نرخ فریم واقعی ویدیو محدود می‌شود—با یک استثنای مهم:

نرخ مؤثر اجرای فراخوانی‌های برگشتی، کمترین نرخ بین نرخ ویدیو و نرخ مرورگر است. این بدان معناست که یک ویدیوی ۲۵ فریم بر ثانیه که در مرورگری که با نرخ ۶۰ هرتز رنگ‌آمیزی می‌کند پخش می‌شود، فراخوانی‌های برگشتی را با نرخ ۲۵ هرتز اجرا می‌کند. یک ویدیوی ۱۲۰ فریم بر ثانیه در همان مرورگر ۶۰ هرتز، فراخوانی‌های برگشتی را با نرخ ۶۰ هرتز اجرا می‌کند.

تشخیص ویژگی

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

پلی‌فیل

یک polyfill برای متد requestVideoFrameCallback() بر اساس Window.requestAnimationFrame() و HTMLVideoElement.getVideoPlaybackQuality() در دسترس است. قبل از استفاده از این، از محدودیت‌های ذکر شده در README آگاه باشید.

از روش استفاده کنید

اگر از متد 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);

در callback، 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 است. پیاده‌سازی کرومیوم از ساعت صوتی به عنوان منبع زمانی که از video.currentTime پشتیبانی می‌کند، استفاده می‌کند، در حالی که mediaTime مستقیماً توسط presentationTimestamp فریم پر می‌شود. mediaTime چیزی است که اگر می‌خواهید فریم‌ها را به روشی قابل تکرار شناسایی کنید، از جمله برای شناسایی دقیق فریم‌هایی که از دست داده‌اید، باید از آن استفاده کنید.

اگر همه چیز یک فریم با هم متفاوت به نظر می‌رسد

همگام‌سازی عمودی (یا فقط vsync)، یک فناوری گرافیکی است که نرخ فریم یک ویدیو و نرخ به‌روزرسانی مانیتور را همگام‌سازی می‌کند. از آنجایی که requestVideoFrameCallback() روی نخ اصلی اجرا می‌شود، اما در باطن، ترکیب ویدیو روی نخ ترکیب‌کننده اتفاق می‌افتد، همه چیز از این API بهترین تلاش است و مرورگر هیچ تضمین دقیقی ارائه نمی‌دهد.

ممکن است API نسبت به زمان رندر شدن یک فریم ویدیویی، یک vsync (همگام‌سازی همزمان) دیرتر انجام شود. برای اینکه تغییرات اعمال شده در صفحه وب از طریق API روی صفحه نمایش داده شوند، یک vsync لازم است (همانند window.requestAnimationFrame() ). بنابراین اگر مرتباً mediaTime یا شماره فریم را در صفحه وب خود به‌روزرسانی کنید و آن را با فریم‌های ویدیویی شماره‌گذاری شده مقایسه کنید، در نهایت ویدیو طوری به نظر می‌رسد که انگار یک فریم جلوتر است.

آنچه واقعاً اتفاق می‌افتد این است که فریم در vsync x آماده است، callback اجرا می‌شود و فریم در vsync x+1 رندر می‌شود و تغییرات ایجاد شده در callback در vsync x+2 رندر می‌شوند. می‌توانید با بررسی اینکه آیا metadata.expectedDisplayTime تقریباً now است یا یک vsync در آینده، بررسی کنید که آیا callback یک vsync دیرهنگام است (و فریم از قبل روی صفحه رندر شده است). اگر در حدود پنج تا ده میکروثانیه از 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() این راه حل را تا حد زیادی بهبود می‌بخشد.

تقدیرنامه‌ها

API requestVideoFrameCallback توسط توماس گیلبرت مشخص و پیاده‌سازی شده است. این پست توسط جو مدلی و کیس باسک بررسی شده است.