איך משתמשים ב-requestVideoFrameCallback()
כדי לעבוד בצורה יעילה יותר עם סרטונים בדפדפן
השיטה HTMLVideoElement.requestVideoFrameCallback()
מאפשרת לכותבי האינטרנט לרשום פונקציית קריאה חוזרת (callback) שתופעל בשלבים של העיבוד כשמסגרת וידאו חדשה נשלחת למעבד הקומפוזיציה.
כך המפתחים יכולים לבצע פעולות יעילות בכל פריים של הסרטון, כמו עיבוד וצביעה של סרטון על קנבס, ניתוח סרטונים או סנכרון עם מקורות אודיו חיצוניים.
ההבדל ביחס ל-requestAnimationFrame()
פעולות כמו ציור של פריים של סרטון על לוח באמצעות 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!
}
תמיכה בדפדפנים
פוליפיל
זמין 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.