כאן מוסבר איך משתמשים ב-requestVideoFrameCallback() כדי לעבוד בצורה יעילה יותר עם סרטונים בדפדפן.
תאריך פרסום: 8 בינואר 2023
השיטה HTMLVideoElement.requestVideoFrameCallback()
מאפשרת למפתחי אתרים לרשום קריאה חוזרת (callback)
שמופעלת בשלבי הרינדור כשפריים חדש של סרטון נשלח למרכיב.
היא מאפשרת למפתחים לבצע פעולות יעילות בכל פריים של סרטון, כמו עיבוד סרטונים וציור על אזור עריכה, ניתוח סרטונים או סנכרון עם מקורות אודיו חיצוניים.
ההבדל בינה לבין requestAnimationFrame()
פעולות שמתבצעות באמצעות ה-API הזה, כמו ציור של פריים של סרטון על קנבס באמצעות
drawImage(),
מסונכרנות כמיטב היכולת עם קצב הפריימים של הסרטון שמופעל על המסך. זה שונה מwindow.requestAnimationFrame(), שבדרך כלל מופעל כ-60 פעמים בשנייה.
requestVideoFrameCallback() קשור לקצב הפריימים בפועל של הסרטון – עם חריג חשוב:
הקצב האפקטיבי שבו מופעלים הקבצים החוזרים הוא הקצב הנמוך מבין הקצב של הסרטון והקצב של הדפדפן. כלומר, אם סרטון של 25 FPS מופעל בדפדפן שמעבד את התצוגה ב-60 Hz, יופעלו קריאות חוזרות ב-25 Hz. סרטון של 120fps באותו דפדפן של 60Hz יפעיל קריאות חוזרות בקצב של 60Hz.
זיהוי תכונות
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);
בפונקציית הקריאה החוזרת, now הוא DOMHighResTimeStamp ו-metadata הוא מילון VideoFrameMetadata עם המאפיינים הבאים:
-
presentationTime, מהסוגDOMHighResTimeStamp: השעה שבה סוכן המשתמש שלח את המסגרת להרכבה. -
expectedDisplayTime, מסוגDOMHighResTimeStamp: השעה שבה סוכן המשתמש מצפה שהמסגרת תהיה גלויה. -
width, of typeunsigned long: רוחב מסגרת הסרטון, בפיקסלים של מדיה. -
height, of typeunsigned long: גובה מסגרת הסרטון, בפיקסלים של המדיה. -
mediaTime, מסוגdouble: חותמת הזמן של הצגת המדיה (PTS) בשניות של המסגרת המוצגת (למשל, חותמת הזמן שלה בציר הזמןvideo.currentTime). -
presentedFrames, מסוגunsigned long: ספירה של מספר המסגרות שנשלחו להרכבה. מאפשר ללקוחות לקבוע אם היו פריימים חסרים בין מופעים שלVideoFrameRequestCallback. -
processingDuration, מסוגdouble: המשך הזמן שחלף בשניות מרגע השליחה של החבילה המקודדת עם חותמת הזמן של ההצגה (PTS) זהה למסגרת הזו (למשל, זהה ל-mediaTime) למפענח עד שהמסגרת המפוענחת הייתה מוכנה להצגה.
באפליקציות WebRTC, יכולים להופיע מאפיינים נוספים:
-
captureTime, מהסוגDOMHighResTimeStamp: עבור פריימים של סרטונים שמגיעים ממקור מקומי או מרוחק, זהו הזמן שבו הפריימים צולמו על ידי המצלמה. במקור מרוחק, זמן הצילום מוערך באמצעות סנכרון שעון ודוחות שולח RTCP כדי להמיר חותמות זמן של RTP לזמן הצילום. -
receiveTime, מהסוגDOMHighResTimeStamp: עבור פריימים של סרטונים שמגיעים ממקור מרוחק, זהו הזמן שבו הפלטפורמה קיבלה את הפריימים המוצפנים, כלומר הזמן שבו התקבל ברשת החבילה האחרונה ששייכת לפריימים האלה. -
rtpTimestamp, of typeunsigned 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.
כדי לבדוק אם הקריאה החוזרת היא מאוחרת ב-vsync (והפריים כבר עבר רינדור במסך), צריך לבדוק אם metadata.expectedDisplayTime הוא בערך now או vsync אחד בעתיד.
אם הוא נמצא בטווח של כ-5 עד 10 מיקרו-שניות מ-now, הפריים כבר עבר רינדור;
אם expectedDisplayTime הוא בערך 16 מילי-שניות בעתיד
(בהנחה שהדפדפן שלכם מתרענן ב-60Hz), אז אתם מסונכרנים עם הפריים.
הדגמה (דמו)
יצרתי הדגמה קטנה שמראה איך פריימים מצוירים על בד בדיוק בקצב הפריימים של הסרטון, ואיפה מטא-נתוני הפריימים נרשמים לצורכי ניפוי באגים.
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() משפרת מאוד את הפתרון העקיף הזה.
תודות
requestVideoFrameCallback API הוגדר והוטמע על ידי Thomas Guilbert.
הפוסט הזה נבדק על ידי Joe Medley
ו-Kayce Basques.