ביצוע מניפולציות על רכיבי וידאו בסטרימינג.
טכנולוגיות אינטרנט מודרניות מספקות דרכים רבות לעבוד עם וידאו. Media Stream API, Media Recording API, Media Source API ו-WebRTC API הם חבילה עשירה של כלים להקלטה, להעברה ולנגינה של שידורי וידאו. הם פותרים משימות מסוימות ברמה גבוהה, אבל הם לא מאפשרים למתכנתי אינטרנט לעבוד עם רכיבים נפרדים של סטרימינג וידאו, כמו פריימים וקטעי וידאו או אודיו מקודדים שלא עברו ניתוק מMUX. כדי לקבל גישה ברמה נמוכה לרכיבים הבסיסיים האלה, מפתחים השתמשו ב-WebAssembly כדי להוסיף לדפדפן קודקים של וידאו ואודיו. עם זאת, מכיוון שדפדפנים מודרניים כבר כוללים מגוון קודקים (שפעמים רבות מואצים על ידי חומרה), האריזה מחדש שלהם כ-WebAssembly נראית כמו בזבוז של משאבי אנוש ומחשבים.
WebCodecs API מבטל את חוסר היעילות הזה בכך שהוא מספק למתכנתים דרך להשתמש ברכיבי מדיה שכבר קיימים בדפדפן. פרטים נוספים:
- מקודדים של וידאו ואודיו
- מקודדים של וידאו ואודיו
- פריימים גולמיים של וידאו
- מפענחים של תמונות
ממשק WebCodecs API שימושי לאפליקציות אינטרנט שדורשות שליטה מלאה על אופן העיבוד של תוכן המדיה, כמו תוכנות לעריכת וידאו, תוכנות לשיחות וידאו, סטרימינג של וידאו וכו'.
תהליך העבודה של עיבוד הסרטונים
פריימים הם הרכיב המרכזי בעיבוד וידאו. לכן ב-WebCodecs רוב המחלקות צורכות או מפיקות פריימים. מקודדי וידאו ממירים פריימים למקטעי נתונים מקודדים. מפענחי וידאו עושים את ההפך.
בנוסף, VideoFrame
תואם לממשקי Web API אחרים כי הוא CanvasImageSource
ויש לו מתכנת שמקבל CanvasImageSource
.
לכן אפשר להשתמש בו בפונקציות כמו drawImage()
ו-texImage2D()
. בנוסף, אפשר ליצור אותו מקנבסים, מ-bitmaps, מאלמנטי וידאו וממסגרות וידאו אחרות.
WebCodecs API פועל היטב בשילוב עם הכיתות מ-Insertable Streams API שמחברות את WebCodecs לטראקים של סטרימינג של מדיה.
MediaStreamTrackProcessor
מפרק טראקים של מדיה לפריימים נפרדים.MediaStreamTrackGenerator
יוצרת טראק מדיה מזרם של פריימים.
WebCodec ועובדי אינטרנט
מעצם הגדרתו, WebCodecs API מבצע את כל העבודה הקשה באופן אסינכרוני מחוץ ל-thread הראשי. עם זאת, מכיוון שקריאות חזרה מסוג frame ו-chunk יכולות להתבצע לעיתים קרובות כמה פעמים בשנייה, הן עלולות להעמיס על השרשור הראשי וכך להפחית את תגובת האתר. לכן, עדיף להעביר את הטיפול בפריימים נפרדים ובקטעים מקודדים ל-web worker.
כדי לעזור בכך, ReadableStream מספק דרך נוחה להעביר באופן אוטומטי את כל המסגרות שמגיעות מטרקי מדיה לעובד. לדוגמה, אפשר להשתמש ב-MediaStreamTrackProcessor
כדי לקבל ReadableStream
לטראק של שידור מדיה שמגיע ממצלמת האינטרנט. לאחר מכן, הסטרימינג מועבר ל-web worker שבו הפריימים נקרא אחד אחרי השני ומצורפים לתור ב-VideoEncoder
.
באמצעות HTMLCanvasElement.transferControlToOffscreen
אפשר לבצע עיבוד גם מחוץ ל-thread הראשי. עם זאת, אם כל הכלים ברמה הגבוהה לא מתאימים לכם, אפשר להעביר את VideoFrame
עצמו בין עובדים.
רכיבי WebCodec בפעולה
קידוד
הכול מתחיל ב-VideoFrame
.
יש שלוש דרכים ליצור פריימים של וידאו.
ממקור תמונה כמו קנבס, קובץ בייטמפי של תמונה או רכיב וידאו.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
אפשר להשתמש ב-
MediaStreamTrackProcessor
כדי לשלוף פריימים מ-MediaStreamTrack
const stream = await navigator.mediaDevices.getUserMedia({…}); const track = stream.getTracks()[0]; const trackProcessor = new MediaStreamTrackProcessor(track); const reader = trackProcessor.readable.getReader(); while (true) { const result = await reader.read(); if (result.done) break; const frameFromCamera = result.value; }
יצירת פריים מהייצוג הבינארי של הפיקסלים ב-
BufferSource
const pixelSize = 4; const init = { timestamp: 0, codedWidth: 320, codedHeight: 200, format: "RGBA", }; const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize); for (let x = 0; x < init.codedWidth; x++) { for (let y = 0; y < init.codedHeight; y++) { const offset = (y * init.codedWidth + x) * pixelSize; data[offset] = 0x7f; // Red data[offset + 1] = 0xff; // Green data[offset + 2] = 0xd4; // Blue data[offset + 3] = 0x0ff; // Alpha } } const frame = new VideoFrame(data, init);
לא משנה מאיפה הם מגיעים, אפשר לקודד את הפריימים לאובייקטים מסוג EncodedVideoChunk
באמצעות VideoEncoder
.
לפני הקידוד, צריך להעביר ל-VideoEncoder
שני אובייקטים של JavaScript:
- הפעלת מילון עם שתי פונקציות לטיפול בקטעי קוד מקודדים ובשגיאות. הפונקציות האלה מוגדרות על ידי המפתח, ואי אפשר לשנות אותן אחרי שהן מועברות למבנה
VideoEncoder
. - אובייקט תצורה של מקודד, שמכיל פרמטרים של מקור הווידאו בפלט. אפשר לשנות את הפרמטרים האלה מאוחר יותר באמצעות קריאה ל-
configure()
.
אם הדפדפן לא תומך בתצורה, השיטה configure()
תגרום להשלכת NotSupportedError
. מומלץ להפעיל את השיטה הסטטית VideoEncoder.isConfigSupported()
עם ההגדרות כדי לבדוק מראש אם היא נתמכת ולחכות עד להבטחה שלה.
const init = {
output: handleChunk,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
width: 640,
height: 480,
bitrate: 2_000_000, // 2 Mbps
framerate: 30,
};
const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
const encoder = new VideoEncoder(init);
encoder.configure(config);
} else {
// Try another config.
}
אחרי שמגדירים את המקודד, הוא מוכן לקבל פריימים באמצעות השיטה encode()
.
גם configure()
וגם encode()
מחזירים מיד בלי להמתין להשלמת העבודה בפועל. הוא מאפשר להוסיף כמה פריימים לתור לקודוקציה בו-זמנית, בעוד ש-encodeQueueSize
מציג את מספר הבקשות שממתינות בתור עד שהקידוד הקודם יסתיים.
הדיווח על שגיאות מתבצע באמצעות הטלת חריגה באופן מיידי, אם הארגומנטים או סדר הקריאות ל-method מפירים את חוזה ה-API, או באמצעות קריאה ל-callback error()
במקרה של בעיות בהטמעת הקודק.
אם ההצפנה מסתיימת בהצלחה, מתבצעת קריאה ל-callback output()
עם מקטע חדש שהוצפן כארגומנטים.
פרט חשוב נוסף כאן הוא שצריך לומר למסגרות כשהן לא נחוצות יותר, באמצעות קריאה ל-close()
.
let frameCounter = 0;
const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);
const reader = trackProcessor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done) break;
const frame = result.value;
if (encoder.encodeQueueSize > 2) {
// Too many frames in flight, encoder is overwhelmed
// let's drop this frame.
frame.close();
} else {
frameCounter++;
const keyFrame = frameCounter % 150 == 0;
encoder.encode(frame, { keyFrame });
frame.close();
}
}
עכשיו הגיע הזמן לסיים את הקוד לקידוד על ידי כתיבת פונקציה שמטפלת בקטעי וידאו מקודדים כשהם יוצאים מהמקודד. בדרך כלל, הפונקציה הזו שולחת מקטעי נתונים ברשת או ממזגת אותם לקונטיינר מדיה לאחסון.
function handleChunk(chunk, metadata) {
if (metadata.decoderConfig) {
// Decoder needs to be configured (or reconfigured) with new parameters
// when metadata has a new decoderConfig.
// Usually it happens in the beginning or when the encoder has a new
// codec specific binary configuration. (VideoDecoderConfig.description).
fetch("/upload_extra_data", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: metadata.decoderConfig.description,
});
}
// actual bytes of encoded data
const chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: chunkData,
});
}
אם בשלב מסוים תצטרכו לוודא שכל הבקשות לקידוד בהמתנה הושלמו, תוכלו להפעיל את flush()
ולהמתין להבטחה שלו.
await encoder.flush();
פענוח הקוד
ההגדרה של VideoDecoder
דומה להגדרה של VideoEncoder
: שתי פונקציות מועברות כשיוצרים את המפענח, ופרמטרים של codec מועברים ל-configure()
.
קבוצת הפרמטרים של הקודק משתנה בהתאם לקודק. לדוגמה, יכול להיות שקודק H.264 יצטרך blob בינארי של AVCC, אלא אם הוא מקודד בפורמט שנקרא Annex B (encoderConfig.avc = { format: "annexb" }
).
const init = {
output: handleFrame,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
codedWidth: 640,
codedHeight: 480,
};
const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
const decoder = new VideoDecoder(init);
decoder.configure(config);
} else {
// Try another config.
}
אחרי שמפעילים את המפענח, אפשר להתחיל להזין אליו אובייקטים מסוג EncodedVideoChunk
.
כדי ליצור מקטע, צריך:
BufferSource
של נתוני וידאו מקודדים- חותמת הזמן של תחילת הרצף במיליוניות השנייה (זמן המדיה של הפריים המקודד הראשון ברצף)
- סוג הקטע, אחד מהערכים הבאים:
key
אם אפשר לפענח את הקטע בנפרד מקטעים קודמיםdelta
אם אפשר לפענח את הקטע רק אחרי פענוח של קטעים קודמים אחד או יותר
בנוסף, כל קטעי הקוד (chunks) שהקודק מפיץ מוכנים למפענח כפי שהם. כל מה שצוין למעלה לגבי דיווח על שגיאות ועל האופי האסינכרוני של השיטות של הקודק נכון גם למפענחים.
const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
const chunk = new EncodedVideoChunk({
timestamp: responses[i].timestamp,
type: responses[i].key ? "key" : "delta",
data: new Uint8Array(responses[i].body),
});
decoder.decode(chunk);
}
await decoder.flush();
עכשיו נסביר איך אפשר להציג בדף מסגרת שקודדה לאחרונה. מומלץ לוודא שהקריאה החוזרת (callback) של הפלט של המפענח (handleFrame()
) תוחזר במהירות. בדוגמה הבאה, היא רק מוסיפה מסגרת לתור של הפריימים שמוכנים לעיבוד.
העיבוד מתבצע בנפרד, והוא מורכב משני שלבים:
- ממתינים לזמן הנכון להצגת המסגרת.
- ציור המסגרת על קנבס.
כשלא צריך יותר פריים, קוראים לפונקציה close()
כדי לשחרר את הזיכרון הבסיסי לפני שאסף האשפה מגיע אליו. הפעולה הזו תקטין את כמות הזיכרון הממוצעת שבה אפליקציית האינטרנט משתמשת.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;
function handleFrame(frame) {
pendingFrames.push(frame);
if (underflow) setTimeout(renderFrame, 0);
}
function calculateTimeUntilNextFrame(timestamp) {
if (baseTime == 0) baseTime = performance.now();
let mediaTime = performance.now() - baseTime;
return Math.max(0, timestamp / 1000 - mediaTime);
}
async function renderFrame() {
underflow = pendingFrames.length == 0;
if (underflow) return;
const frame = pendingFrames.shift();
// Based on the frame's timestamp calculate how much of real time waiting
// is needed before showing the next frame.
const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
await new Promise((r) => {
setTimeout(r, timeUntilNextFrame);
});
ctx.drawImage(frame, 0, 0);
frame.close();
// Immediately schedule rendering of the next frame
setTimeout(renderFrame, 0);
}
טיפים למפתחים
אתם יכולים להשתמש בלוח המדיה בכלי הפיתוח ל-Chrome כדי להציג יומני מדיה ולפתור באגים ב-WebCodecs.
הדגמה (דמו)
בהדגמה הבאה מוצג איך מסגרות אנימציה מ-Canvas:
- צולם ב-25fps ב-
ReadableStream
על ידיMediaStreamTrackProcessor
- מועברים ל-web worker
- מקודדים בפורמט וידאו H.264
- מקודדים מחדש לסדרה של פריימים של וידאו.
- ועוברים עיבוד בלוח הציור השני באמצעות
transferControlToOffscreen()
הדגמות נוספות
כדאי גם לעיין בהדגמות הנוספות שלנו:
שימוש ב-WebCodecs API
זיהוי תכונות
כדי לבדוק אם יש תמיכה ב-WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
חשוב לזכור ש-WebCodecs API זמין רק בהקשרים מאובטחים, כך שהזיהוי ייכשל אם הערך של self.isSecureContext
הוא False.
משוב
צוות Chrome רוצה לשמוע על החוויות שלך בשימוש ב-WebCodecs API.
תיאור של עיצוב ה-API
האם יש משהו ב-API שלא פועל כצפוי? או שאולי חסרות שיטות או מאפיינים שדרושים לכם כדי ליישם את הרעיון? יש לכם שאלות או הערות לגבי מודל האבטחה? אפשר לשלוח דיווח על בעיה ב-spec במאגר GitHub המתאים, או להוסיף את המחשבות שלכם לבעיה קיימת.
דיווח על בעיה בהטמעה
מצאתם באג בהטמעה של Chrome? או שההטמעה שונה מהמפרט? דווחו על באג בכתובת new.crbug.com. הקפידו לכלול כמה שיותר פרטים, הוראות פשוטות לשחזור הבאג ולהזין Blink>Media>WebCodecs
בתיבה רכיבים.
Glitch הוא כלי מצוין לשיתוף שחזור מהיר וקל של באגים.
תמיכה ב-API
האם אתם מתכננים להשתמש ב-WebCodecs API? התמיכה הציבורית שלכם עוזרת לצוות Chrome לתעדף תכונות ומראה לספקי דפדפנים אחרים עד כמה חשוב לתמוך בהם.
אפשר לשלוח אימיילים לכתובת media-dev@chromium.org או לשלוח ציוץ אל @ChromiumDev באמצעות ה-hashtag #WebCodecs
ולספר לנו איפה ואיך אתם משתמשים בו.