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

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

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

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

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