שינוי של רכיבי וידאו בסטרימינג.
טכנולוגיות אינטרנט מודרניות מספקות דרכים רבות לעבוד עם סרטונים. Media Stream API, Media Recording API, Media Source API ו-WebRTC API הם כלים שימושיים להקלטה, להעברה ולהפעלה של סטרימינג של וידאו. בזמן פתרון משימות מסוימות ברמה גבוהה, ממשקי ה-API האלה לא מאפשרים למתכנתי אינטרנט לעבוד עם רכיבים נפרדים של סטרימינג וידאו, כמו פריימים וקטעים לא מעורבבים של וידאו או אודיו מקודדים. כדי לקבל גישה ברמה נמוכה לרכיבים הבסיסיים האלה, מפתחים השתמשו ב-WebAssembly כדי להוסיף קודקים של וידאו ואודיו לדפדפן. אבל בהתחשב בכך שדפדפנים מודרניים כבר מגיעים עם מגוון רחב של קודקים (שבדרך כלל מואצים על ידי חומרה), אריזה מחדש שלהם כ-WebAssembly נראית כמו בזבוז של משאבים אנושיים ומחשובים.
WebCodecs API מאפשר למתכנתים להשתמש ברכיבי מדיה שכבר קיימים בדפדפן, וכך מייעל את התהליך. באופן ספציפי:
- מפענחי וידאו ואודיו
- מקודדי וידאו ואודיו
- פריימים גולמיים של סרטונים
- פענוח הקוד של התמונה
ממשק WebCodecs API שימושי לאפליקציות אינטרנט שנדרשת בהן שליטה מלאה באופן העיבוד של תוכן מדיה, כמו עורכי וידאו, שיחות וידאו, סטרימינג של וידאו וכו'.
תהליך העבודה של עיבוד סרטונים
הפריימים הם מרכיב מרכזי בעיבוד סרטונים. לכן, ב-WebCodecs רוב המחלקות צורכות או מייצרות פריימים. מקודדי וידאו ממירים פריימים לחלקים מקודדים. מפענחי וידאו עושים את הפעולה ההפוכה.
בנוסף, VideoFrame
פועל בצורה חלקה עם ממשקי API אחרים לאינטרנט, כי הוא CanvasImageSource
ויש לו constructor שמקבל CanvasImageSource
.
לכן אפשר להשתמש בה בפונקציות כמו drawImage()
ו-texImage2D()
. בנוסף, אפשר ליצור אותו מבד ציור, מפות סיביות, רכיבי וידאו ומסגרות וידאו אחרות.
WebCodecs API פועל היטב בשילוב עם המחלקות מ-Insertable Streams API, שמקשרות בין WebCodecs לבין רכיבי מדיה של סטרימינג.
-
MediaStreamTrackProcessor
מחלק את רצועות המדיה למסגרות נפרדות. -
MediaStreamTrackGenerator
יוצרת טראק מדיה מזרם של פריימים.
WebCodecs ו-web workers
ממשק WebCodecs API נועד לבצע את כל הפעולות הכבדות באופן אסינכרוני מחוץ ל-thread הראשי. אבל מכיוון שאפשר להפעיל את הפונקציות של callback של פריימים ושל חלקי נתונים כמה פעמים בשנייה, הן עלולות להעמיס על ה-thread הראשי וכך להפחית את מהירות התגובה של האתר. לכן, עדיף להעביר את הטיפול בפריימים בודדים ובחלקים מקודדים אל web worker.
כדי לעזור בכך, ReadableStream מספק דרך נוחה להעברה אוטומטית של כל הפריים שמגיע ממסלול מדיה אל העובד. לדוגמה, אפשר להשתמש ב-MediaStreamTrackProcessor
כדי לקבל ReadableStream
עבור טראק של סטרימינג מדיה שמגיע ממצלמת האינטרנט. לאחר מכן, הזרם מועבר ל-Web Worker שבו מסגרות נקראות אחת אחת ומתווספות לתור ב-VideoEncoder
.
עם HTMLCanvasElement.transferControlToOffscreen
אפשר לבצע רינדור גם מחוץ ל-thread הראשי. אבל אם כל הכלים ברמה הגבוהה לא נוחים, אפשר להעביר את VideoFrame
עצמו בין העובדים.
WebCodecs בפעולה
קידוד

Canvas
או מ-ImageBitmap
לרשת או לאחסוןהכול מתחיל ב-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:
- מאתחלים את המילון עם שתי פונקציות לטיפול בחלקים מקודדים ובשגיאות. הפונקציות האלה מוגדרות על ידי המפתח ואי אפשר לשנות אותן אחרי שהן מועברות אל ה-constructor של
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
מראה כמה בקשות ממתינות בתור לסיום הקידודים הקודמים.
השגיאות מדווחות על ידי הפעלת חריגה באופן מיידי, במקרה שהארגומנטים או הסדר של קריאות השיטה מפרים את חוזה ה-API, או על ידי קריאה ל-callback של error()
לבעיות שנתקלו בהן בהטמעה של codec.
אם הקידוד מסתיים בהצלחה, מתבצעת קריאה חוזרת (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();
פענוח

Canvas
או אל ImageBitmap
.ההגדרה של VideoDecoder
דומה להגדרה של VideoEncoder
: שתי פונקציות מועברות כשיוצרים את המפענח, ופרמטרים של קודק מועברים אל 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
אם אפשר לפענח את הנתח רק אחרי שפוענחו נתחים קודמים
-
בנוסף, כל חלקי הנתונים שנוצרים על ידי המקודד מוכנים לשימוש במפענח כמו שהם. כל מה שנאמר למעלה על דיווח שגיאות ועל האופי האסינכרוני של השיטות של המקודד נכון גם לגבי מפענחים.
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();
עכשיו נראה איך אפשר להציג בדף פריים שפוענח לאחרונה. מומלץ לוודא שפונקציית הקריאה החוזרת של פלט המפענח (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.

הדגמה (דמו)
ההדגמה מראה איך מסגרות אנימציה מלוח ציור:
- צולם ב-25 פריימים לשנייה ב-
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 שלא פועל כמו שציפית? או שחסרות שיטות או מאפיינים שצריך להטמיע כדי לממש את הרעיון? יש לך שאלה או הערה לגבי מודל האבטחה? אפשר להגיש בקשה לבעיה במפרט במאגר GitHub המתאים, או להוסיף את המחשבות שלכם לבעיה קיימת.
דיווח על בעיה בהטמעה
מצאתם באג בהטמעה של Chrome? או שההטמעה שונה מהמפרט? מדווחים על הבאג בכתובת new.crbug.com. חשוב לכלול כמה שיותר פרטים, הוראות פשוטות לשחזור הבאג ומזינים Blink>Media>WebCodecs
בתיבה Components.
תמיכה ב-API
האם בכוונתך להשתמש ב-WebCodecs API? התמיכה הציבורית שלכם עוזרת לצוות Chrome לתת עדיפות לתכונות, ומראה לספקי דפדפנים אחרים עד כמה חשוב לתמוך בהן.
אפשר לשלוח אימייל לכתובת media-dev@chromium.org או לצייץ לכתובת @ChromiumDev עם ההאשטאג #WebCodecs
ולספר לנו איפה ואיך אתם משתמשים בו.