כאן מוסבר איך משתמשים ב-requestVideoFrameCallback()
כדי לעבוד בצורה יעילה יותר עם סרטונים בדפדפן.
השיטה HTMLVideoElement.requestVideoFrameCallback()
מאפשרת למפתחי אתרים לרשום קריאה חוזרת (callback) שמופעלת בשלבי הרינדור כשפריים חדש של סרטון נשלח למרכיב.
כך מפתחים יכולים לבצע פעולות יעילות בכל פריים של סרטון, כמו עיבוד סרטונים וציור על אזור עריכה, ניתוח סרטונים או סנכרון עם מקורות אודיו חיצוניים.
ההבדל בינה לבין requestAnimationFrame()
פעולות כמו ציור פריים של סרטון על בד ציור באמצעות
drawImage()
שמתבצעות דרך ה-API הזה יסונכרנו כמיטב היכולת
עם קצב הפריימים של הסרטון שמופעל על המסך.
שונה מ-window.requestAnimationFrame()
, שמופעל בדרך כלל כ-60 פעמים בשנייה, requestVideoFrameCallback()
קשור לקצב הפריימים בפועל של הסרטון – עם חריג חשוב:
הקצב האפקטיבי שבו מופעלות קריאות חוזרות הוא הקצב הנמוך מבין הקצב של הסרטון והקצב של הדפדפן. כלומר, אם סרטון עם 25 פריימים לשנייה מופעל בדפדפן שמעבד את התצוגה ב-60 הרץ, הקריאות החוזרות יופעלו ב-25 הרץ. סרטון של 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()
.
רושמים פונקציית 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);
בקריאה החוזרת, now
הוא DOMHighResTimeStamp
ו-metadata
הוא מילון VideoFrameMetadata
עם המאפיינים הבאים:
-
presentationTime
, מהסוגDOMHighResTimeStamp
: השעה שבה סוכן המשתמש שלח את המסגרת להרכבה. -
expectedDisplayTime
, מסוגDOMHighResTimeStamp
: השעה שבה סוכן המשתמש מצפה שהמסגרת תהיה גלויה. -
width
, מסוגunsigned 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()
פועל בשרשור הראשי, אבל מתחת לפני השטח, שילוב הווידאו מתבצע בשרשור המרכיב, כל מה שקשור ל-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), אז אתם מסונכרנים עם הפריים.
הדגמה (דמו)
יצרתי הדגמה קטנה שמראה איך פריימים מצוירים על בד בדיוק בקצב הפריימים של הסרטון, ואיפה מטא-נתוני הפריימים מתועדים למטרות ניפוי באגים.
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.
תמונה ראשית (Hero) מאת
Denise Jans ב-Unsplash.