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

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

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

ההבדל עם requestAnimationFrame()

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

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

איזה שם?

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

זיהוי תכונות

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

תמיכת דפדפן

תמיכה בדפדפן

  • 83
  • 83
  • x
  • 15.4

מקור

פוליפיל

יש פוליפיל לשיטה requestVideoFrameCallback() שמבוסס על Window.requestAnimationFrame() ועל HTMLVideoElement.getVideoPlaybackQuality(). לפני שמשתמשים באפשרות הזו, חשוב לשים לב למגבלות שצוינו בREADME.

שימוש בשיטה requestVideoFrameCallback()

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

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. בהטמעה של Chromium נעשה שימוש בשעון האודיו כמקור הזמן שמגבה את video.currentTime, בעוד שה-mediaTime מאוכלס ישירות על ידי presentationTimestamp של הפריים. כדאי להשתמש ב-mediaTime אם רוצים לזהות בדיוק פריימים באופן שניתן לשחזר, כולל כדי לזהות בדיוק אילו פריימים פספסתם.

אם נראה שפריים אחד לא מופיע...

סנכרון אנכי (או פשוט vsync) הוא טכנולוגיה גרפית שמסנכרנת את קצב הפריימים של סרטון ואת קצב הרענון של צג. מכיוון ש-requestVideoFrameCallback() פועל ב-thread הראשי, אבל מאחורי הקלעים, הרכבה של סרטונים מתבצעת בשרשור של המחבר, לכן כל מה שב-API הזה מומלץ, והדפדפן לא מציע אחריות מחמירה. מה שעלול לקרות הוא שה-API יכול להיות Vsync מאוחר אחד ביחס לזמן עיבוד הפריים של הסרטון. נדרש vsync אחד לשינויים שבוצעו בדף האינטרנט דרך ה-API כדי להופיע על המסך (כמו ב-window.requestAnimationFrame()). כך שאם ממשיכים לעדכן את mediaTime או את מספר המסגרת בדף האינטרנט ומשווים אותם לפריימים הממוספרים של הסרטון, בסופו של דבר ייראה הסרטון כאילו הוא נמצא בקצב של פריים אחד.

מה שבאמת קורה הוא שהפריים מוכן ב-vsync x, הקריאה החוזרת מופעלת והמסגרת מעובדת ב-vsync x+1 ושינויים שמתבצעים בקריאה החוזרת מעובדים ב-vsync x+2. כדי לבדוק אם הקריאה החוזרת היא vsync מאוחרת (והפריים כבר מעובד במסך), אפשר לבדוק אם ה-metadata.expectedDisplayTime הוא בערך now או vsync אחד בעתיד. אם הזמן הוא בטווח של כחמש עד עשר מיקרו-שניות מ-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() משפרת מאוד את הפתרון הזה.

אישורים

ה-API של requestVideoFrameCallback צוין ויושם על ידי תומאס גילברט. הפוסט הזה נכתב על ידי Joe Medley ו-Kayce Basques. תמונה ראשית (Hero) מאת דניז ג'אנס ב-UnFlood.