ביצוע פעולות יעילות לכל פריים של סרטון בסרטון באמצעות requestVideoFrameCallback()

איך משתמשים ב-requestVideoFrameCallback() כדי לעבוד בצורה יעילה יותר עם סרטונים בדפדפן

השיטה HTMLVideoElement.requestVideoFrameCallback() מאפשרת לכותבי האינטרנט לרשום פונקציית קריאה חוזרת (callback) שתופעל בשלבים של העיבוד כשמסגרת וידאו חדשה נשלחת למעבד הקומפוזיציה. כך המפתחים יכולים לבצע פעולות יעילות בכל פריים של הסרטון, כמו עיבוד וצביעה של סרטון על קנבס, ניתוח סרטונים או סנכרון עם מקורות אודיו חיצוניים.

פעולות כמו ציור של פריים של סרטון על לוח באמצעות drawImage() שבוצעו דרך ה-API הזה יסונכרנו כמיטב יכולת עם קצב הפריימים של הסרטון שמוצג במסך. בניגוד ל-window.requestAnimationFrame(), שמופעל בדרך כלל כ-60 פעמים בשנייה, האירוע requestVideoFrameCallback() קשור לקצב הפריימים בפועל של הסרטון, עם החרגה חשובה:

הקצב בפועל שבו פונקציות ה-call back מופעלות הוא הקצב הנמוך מבין הקצב של הסרטון והקצב של הדפדפן. כלומר, סרטון שפועל ב-25fps בדפדפן שצובע ב-60Hz יפעיל קריאות חזרה ב-25Hz. סרטון בקצב פריימים של 120FPS באותו דפדפן של 60Hz יפעיל קריאות חזרה ב-60Hz.

מה שם העסק אומר?

בגלל הדמיון ל-window.requestAnimationFrame(), השיטה הוצעה בהתחלה בתור video.requestAnimationFrame() ושמה שונה ל-requestVideoFrameCallback(), אחרי דיון ארוך.

זיהוי תכונות

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

תמיכה בדפדפנים

תמיכה בדפדפנים

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

מקור

פוליפיל

זמין 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() פועל בשרשור הראשי, אבל ברקע, שילוב הסרטונים מתבצע בשרשור המאגר, כל מה שמגיע מ-API הזה הוא לפי מיטב יכולתנו, והדפדפן לא מציע הבטחות מחמירות. יכול להיות שה-API מופעל באיחור של vsync אחד ביחס לזמן שבו פריימים של וידאו עוברים רינדור. כדי שהשינויים שבוצעו בדף האינטרנט דרך ה-API יופיעו במסך, נדרש אירוע vsync אחד (כמו window.requestAnimationFrame()). לכן, אם תמשיכו לעדכן את mediaTime או את מספר המסגרת בדף האינטרנט ולהשוות אותו למסגרות הסרטון הממוספרות, בסופו של דבר הסרטון ייראה כאילו הוא מקדימה מסגרת אחת.

מה שקורה בפועל הוא שהפריים מוכן ב-vsync x, פונקציית ה-callback מופעלת והפריים מומר ב-vsync x+1, והשינויים שבוצעו בפונקציית ה-callback מומרים ב-vsync x+2. כדי לבדוק אם קריאת החזרה (callback) מתבצעת באיחור של vsync (והפריים כבר עבר עיבוד במסך), צריך לבדוק אם הערך של metadata.expectedDisplayTime הוא בערך now או vsync אחד בעתיד. אם הוא נמצא בטווח של כ-5 עד 10 מיקרו-שניות מ-now, הפריים כבר עבר עיבוד. אם הערך של expectedDisplayTime הוא כ-16 אלפיות שנייה בעתיד (בהנחה שהדפדפן או המסך שלכם מתעדכנים בקצב של 60Hz), סימן שאתם מסונכרנים עם הפריים.

הדגמה (דמו)

יצרתי דמו קטן ב-Glitch שמראה איך הפריימים נמתחים על קנבס בדיוק בקצב הפריימים של הסרטון, ואיפה מתועדים ביומן המטא-נתונים של הפריימים למטרות ניפוי באגים.

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() משפרת מאוד את הפתרון החלופי הזה.

תודות

Thomas Guilbert הגדיר והטמיע את ממשק ה-API requestVideoFrameCallback. הפוסט הזה נבדק על ידי Joe Medley ו-Kayce Basques. תמונה ראשית (Hero) של Denise Jans ב-Unsplash.