ביצוע מניפולציות על הרכיבים של שידור הווידאו.
טכנולוגיות אינטרנט מודרניות מציעות דרכים רבות לעבודה עם סרטונים. Media Stream API, Media Recording API, Media Source API ו-WebRTC API מתווספים לקבוצת כלים עשירה להקלטה, להעברה ולהפעלה של שידורי וידאו. בזמן פתרון משימות מסוימות ברמה גבוהה, ממשקי ה-API האלה לא מאפשרים למתכנתים לעבוד עם רכיבים נפרדים של וידאו בסטרימינג, כמו פריימים ומקטעים לא מעורבים של וידאו או אודיו מקודדים. כדי לקבל גישה ברמה נמוכה לרכיבים הבסיסיים האלה, מפתחים משתמשים ב-WebAssembly כדי להכניס קודקי וידאו ואודיו לדפדפן. אבל מכיוון שדפדפנים מודרניים כבר כוללים מגוון של רכיבי Codec (לרוב מואצת על ידי חומרה), האריזה שלהם מחדש כ-WebAssembly נראית כמו בזבוז של משאבים אנושיים ומחשבים.
השימוש ב-WebCodecs API מבטל את היעילות הזו בכך שהוא מאפשר למתכנתים להשתמש ברכיבי מדיה שכבר קיימים בדפדפן. פרטים נוספים:
- מפענחי וידאו ואודיו
- מקודדים של וידאו ואודיו
- פריימים של וידאו גולמיים
- מפענחי תמונות
ה-WebCodecs API שימושי לאפליקציות אינטרנט שמחייבות שליטה מלאה על אופן העיבוד של תוכן מדיה, כמו עורכי וידאו, שיחות ועידה בווידאו, סטרימינג של וידאו וכו'.
תהליך עבודה של עיבוד וידאו
פריימים הם החלק המרכזי בעיבוד וידאו. כך, ב-WebCodecs, רוב המחלקות צורכות או מייצרים מסגרות. מקודדי וידאו ממירים פריימים למקטעים מקודדים. מפענחי וידאו עושים את ההפך.
בנוסף, VideoFrame
פועל היטב עם ממשקי API אחרים באינטרנט באמצעות היותו CanvasImageSource
ויש לו בנאי שמקבל את CanvasImageSource
.
לכן אפשר להשתמש בה בפונקציות כמו drawImage()
ו-texImage2D()
. ניתן גם לבנות אותו מהדפסות על קנבס, מפות סיביות (bitmaps), רכיבי וידאו ופריימים אחרים של וידאו.
ה-WebCodecs API פועל היטב יחד עם המחלקות מ-Insertable Streams API, שמחברות רכיבי WebCodec לטראקים של סטרימינג של מדיה.
- השיטה
MediaStreamTrackProcessor
מפצלת טראקים של מדיה לפריימים נפרדים. - השיטה
MediaStreamTrackGenerator
יוצרת טראק מדיה מזרם של פריימים.
רכיבי WebCodec ועובדי אינטרנט
ה-WebCodecs API החדש עובר את כל העבודה הקשה באופן אסינכרוני ומחוצה ל-thread הראשי. אבל מכיוון שלעיתים קרובות ניתן לקרוא לקריאות חוזרות של מסגרת ומקטעים מספר פעמים בשנייה, הן עשויות להעמיס את ה-thread הראשי וכתוצאה מכך לפגוע בתגובת האתר. לכן עדיף להעביר את הטיפול במסגרות נפרדות ומקטעים מקודדים ל-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:
- הפעלת מילון עם שתי פונקציות לטיפול במקטעים מקודדים ובשגיאות. הפונקציות האלה מוגדרות על ידי המפתח, ואי אפשר לשנות אותן אחרי שהן מועברות ל-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
מציגה את מספר הבקשות שממתינות בתור להשלמת הקידודים הקודמים.
הדיווח על שגיאות מתבצע על ידי העברה מיידית של חריג, למקרה שהארגומנטים או הסדר של הקריאות ל-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
: בתהליך יצירת המפענח מועברות שתי פונקציות, והפרמטרים של הקודק ניתנים ל-configure()
.
קבוצת הפרמטרים של הקודק משתנה מקודק למקודד. לדוגמה, יכול להיות שה-codec 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();
עכשיו הגיע הזמן להראות איך אפשר להציג בדף מסגרת מפוענחת חדשה. כדאי לוודא שהקריאה החוזרת (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);
}
טיפים למפתחים
השתמשו בMedia Panel בכלי הפיתוח ל-Chrome כדי להציג יומני מדיה ולנפות באגים ב-WebCodecs.
הדגמה (דמו)
בהדגמה הבאה אפשר לראות איך פריימים של אנימציה מלוח הציור:
- צולם ב-25FPS ל-
ReadableStream
על ידיMediaStreamTrackProcessor
- הועברה ל-Web worker
- לקודד לפורמט וידאו H.264
- מפוענח שוב לרצף של פריימים של וידאו
- ועובד בתמונה על קנבס השני באמצעות
transferControlToOffscreen()
הדגמות אחרות
נסה גם את ההדגמות האחרות שלנו:
שימוש ב-WebCodecs API
זיהוי תכונות
כדי לבדוק אם יש תמיכה ב-WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
חשוב לזכור: WebCodecs API זמין רק בהקשרים מאובטחים, ולכן הזיהוי ייכשל אם הערך של self.isSecureContext
שגוי.
משוב
צוות Chrome ישמח לשמוע על החוויה שלך עם WebCodecs API.
לספר לנו על עיצוב ה-API
האם יש משהו ב-API שלא פועל כמצופה? או האם חסרים שיטות או מאפיינים שנדרשים לכם כדי ליישם את הרעיון? יש לכם שאלה או הערה לגבי מודל האבטחה? שלחו בעיה במפרט במאגר GitHub המתאים, או הוסיפו את דעתכם לבעיה קיימת.
דיווח על בעיה בהטמעה
האם מצאת באג בהטמעה של Chrome? או שההטמעה
שונה מהמפרט? אפשר לדווח על באג בכתובת new.crbug.com.
הקפידו לכלול כמה שיותר פרטים, הוראות פשוטות לשחזור, ולהזין Blink>Media>WebCodecs
בתיבה רכיבים.
גליץ' הוא כלי מעולה לשיתוף גיבויים מהירים וקלים.
הבעת תמיכה ב-API
האם בכוונתך להשתמש ב-WebCodecs API? התמיכה הציבורית עוזרת לצוות של Chrome לקבוע סדרי עדיפות לתכונות, ומראה לספקי דפדפנים אחרים עד כמה חשוב התמיכה בהן.
עליך לשלוח אימיילים לכתובת media-dev@chromium.org או לשלוח ציוץ לכתובת @ChromiumDev באמצעות ה-hashtag
#WebCodecs
ולהודיע לנו איפה ואיך אתם משתמשים בו.
תמונה ראשית (Hero) מאת דניז ג'נס ב-UnFlood.