ביצוע מניפולציות על רכיבים של וידאו בסטרימינג.
טכנולוגיות אינטרנט מודרניות מספקות דרכים רבות לעבוד עם וידאו. Media Stream API, Media Recording API, Media Source API, ותוסף WebRTC API הוא כלי עשיר להקלטה, להעברה ולהפעלת וידאו בסטרימינג. אמנם הם פותרים משימות מסוימות ברמה גבוהה, אבל ממשקי ה-API האלה לא מאפשרים מתכנתים עובדים עם רכיבים בודדים בסטרימינג של וידאו, כמו פריימים. ומקטעים לא מוקפים של וידאו או אודיו מקודדים. כדי לקבל גישה ברמה נמוכה לרכיבים הבסיסיים האלה, מפתחים השתמשו WebAssembly כדי להכניס לדפדפן קודקים של וידאו ואודיו. אבל בהינתן שדפדפנים מודרניים כבר שולחים עם מגוון של רכיבי קודק (שלרוב מואץ על ידי חומרה), לארוז אותם מחדש כי WebAssembly נראה כמו בזבוז במשאבי אנוש ובמחשבים.
השימוש ב-WebCodecs API מבטל את חוסר היעילות הזו באמצעות מתן אפשרות למתכנתים להשתמש ברכיבי מדיה שכבר קיימים בדפדפן. פרטים נוספים:
- מפענחים של וידאו ואודיו
- מקודדים של וידאו ואודיו
- פריימים גולמיים של וידאו
- מפענחי תמונות
WebCodecs API שימושי עבור אפליקציות אינטרנט המחייבות שליטה מלאה אופן העיבוד של תוכן מדיה, למשל עורכי וידאו, שיחות ועידה בווידאו, סרטונים סטרימינג וכו'.
תהליך עבודה של עיבוד וידאו
פריימים הם החלק המרכזי בעיבוד וידאו. לדוגמה, ברוב הכיתות ב-WebCodec לצרוך או ליצור פריימים. מקודדי וידאו ממירים פריימים למקודדים של מקטעים. מפענחי וידאו עושים את ההפך.
בנוסף, VideoFrame
יכול להתנהג יפה עם ממשקי API אחרים באינטרנט מכיוון שהוא CanvasImageSource
ויש לו constructor שמקבל את CanvasImageSource
.
לכן אפשר להשתמש בו בפונקציות כמו drawImage()
ו-texImage2D()
. כמו כן, ניתן להרכיב אותו מרקעים, ממפות סיביות, מרכיבי וידאו ומפריימים אחרים.
WebCodecs API פועל היטב במקביל למחלקות של Insertable Streams API שמחברים רכיבי WebCodec אל טראקים של סטרימינג של מדיה.
MediaStreamTrackProcessor
מפרק טראקים של מדיה לפריימים נפרדים.MediaStreamTrackGenerator
יוצר טראק של מדיה מרצף של פריימים.
WebCodec ועובדי אינטרנט
על ידי עיצוב, WebCodecs API מבצע את כל העבודה הקשה באופן אסינכרוני ומחוץ ל-thread הראשי. אבל מכיוון שבדרך כלל אפשר לקרוא קריאה חוזרת (callback) של פריימים ומקטעי נתונים מספר פעמים בשנייה, הם עלולים להעמיס על השרשור העיקרי באופן כזה שהאתר יהיה פחות רספונסיבי. לכן עדיף להעביר טיפול במסגרות בודדות ובמקטעים מקודדים Web Worker.
כדי לעזור בכך, ReadableStream
מספק דרך נוחה להעביר באופן אוטומטי את כל הפריימים המגיעים ממדיה
את המסלול לעובד. לדוגמה, אפשר להשתמש ב-MediaStreamTrackProcessor
כדי לקבל
ReadableStream
לטראק של שידור מדיה שמגיע ממצלמת האינטרנט. לאחר מכן
השידור מועבר לעובד באינטרנט שבו המסגרות קוראות אחת אחרי השנייה ועוברות בתור
לVideoEncoder
.
עם HTMLCanvasElement.transferControlToOffscreen
אפשר לבצע רינדור גם מחוץ לשרשור הראשי. אבל אם כל הכלים ברמה גבוהה
לא נוח, 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, או באמצעות קריאה אל error()
קריאה חוזרת (callback) לפתרון בעיות בהטמעת קודק.
אם הקידוד הושלם בהצלחה, output()
הקריאה החוזרת (callback) היא עם מקטע מקודד חדש כארגומנט.
פרט חשוב נוסף כאן הוא שצריך לומר למסגרות כשהן לא
נדרש זמן רב יותר בהתקשרות אל 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, אלא אם הוא מקודד בפורמט נספח 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
אם אפשר לפענח את המקטע רק אחרי פיענוח של מקטע קודם אחד או יותר.
כמו כן, כל המקטעים שהמקודד פולט מוכנים למפענח כפי שהם. כל מה שנאמר למעלה על דיווח שגיאות ועל אופי אסינכרוני של ה-methods של המקודד, מתאימות באותה מידה גם למפענחים.
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 כדי להציג יומני מדיה וניפוי באגים בקודק WebCodec.
הדגמה (דמו)
ההדגמה הבאה מראה איך מסגרות אנימציה מתוך אזור העריכה:
- צילום של
MediaStreamTrackProcessor
בקצב של 25FPS לתוךReadableStream
- הועברה לעובד/ת אינטרנט
- מקודד בפורמט וידאו 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
בתיבה רכיבים.
Glitch היא אפשרות טובה לשיתוף תגובות מהירות וקלות.
הצגת תמיכה ב-API
האם בכוונתך להשתמש ב-WebCodecs API? התמיכה הציבורית שלך עוזרת צוות Chrome מתעדף את התכונות ומראה לספקי דפדפנים אחרים עד כמה הם חשובים היא לתמוך בהם.
שולחים אימיילים אל media-dev@chromium.org או שולחים ציוץ
אל @ChromiumDev באמצעות hashtag
#WebCodecs
וספר לנו איפה אתה משתמש בו ובאיזה אופן.