دستکاری اجزای جریان ویدئو.
فن آوری های وب مدرن راه های زیادی برای کار با ویدیو ارائه می دهند. Media Stream API ، Media Recording API ، Media Source API و WebRTC API به مجموعه ابزاری غنی برای ضبط، انتقال و پخش جریانهای ویدئویی اضافه میشوند. در حین حل برخی از وظایف سطح بالا، این APIها به برنامه نویسان وب اجازه نمی دهند با اجزای مجزای یک جریان ویدیویی مانند فریم ها و تکه های غیرمکس شده ویدیو یا صدای رمزگذاری شده کار کنند. برای دسترسی سطح پایین به این مؤلفههای اساسی، توسعهدهندگان از WebAssembly برای آوردن کدکهای ویدیویی و صوتی به مرورگر استفاده میکنند. اما با توجه به اینکه مرورگرهای مدرن در حال حاضر با انواع کدک ها (که اغلب توسط سخت افزار تسریع می شوند) عرضه می شوند، بسته بندی مجدد آنها به عنوان WebAssembly به نظر اتلاف منابع انسانی و رایانه ای است.
WebCodecs API این ناکارآمدی را با دادن راهی به برنامه نویسان برای استفاده از مؤلفه های رسانه ای که قبلاً در مرورگر وجود دارد، از بین می برد. به طور مشخص:
- رسیورهای صوتی و تصویری
- رمزگذارهای تصویری و صوتی
- فریم های ویدئویی خام
- رسیورهای تصویر
WebCodecs API برای برنامه های کاربردی وب که نیاز به کنترل کامل بر نحوه پردازش محتوای رسانه دارند، مانند ویرایشگرهای ویدئو، کنفرانس ویدئویی، پخش ویدئو و غیره مفید است.
گردش کار پردازش ویدیو
فریم ها محور اصلی پردازش ویدیو هستند. بنابراین در WebCodec ها اکثر کلاس ها یا فریم مصرف می کنند یا تولید می کنند. رمزگذارهای ویدیویی فریم ها را به تکه های کدگذاری شده تبدیل می کنند. رمزگشاهای ویدئویی برعکس عمل می کنند.
همچنین VideoFrame
از طریق CanvasImageSource
بودن و داشتن سازنده ای که CanvasImageSource
می پذیرد، به خوبی با سایر API های وب بازی می کند. بنابراین می توان از آن در توابعی مانند drawImage()
و texImage2D()
استفاده کرد. همچنین می توان آن را از بوم، بیت مپ، عناصر ویدئویی و سایر فریم های ویدئویی ساخت.
WebCodecs API به خوبی در کنار کلاسهای Insertable Streams API که WebCodecs را به آهنگهای جریان رسانه متصل میکند، کار میکند.
-
MediaStreamTrackProcessor
آهنگ های رسانه را به فریم های جداگانه تقسیم می کند. -
MediaStreamTrackGenerator
یک آهنگ رسانه ای را از یک جریان فریم ایجاد می کند.
WebCodec ها و کارگران وب
با طراحی WebCodecs API تمام کارهای سنگین را به صورت ناهمزمان و خارج از رشته اصلی انجام می دهد. اما از آنجایی که اغلب میتوان تماسهای فریم و تکهای را چندین بار در ثانیه فراخوانی کرد، ممکن است موضوع اصلی را به هم ریخته و در نتیجه وبسایت را کمتر پاسخگو کنند. بنابراین ترجیح داده میشود که مدیریت فریمها و تکههای کدگذاریشده را به یک وبکارگر منتقل کنید.
برای کمک به این امر، 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);
مهم نیست که فریم ها از کجا می آیند، فریم ها را می توان با یک VideoEncoder
در اشیاء EncodedVideoChunk
کدگذاری کرد.
قبل از رمزگذاری، باید به VideoEncoder
دو شی جاوا اسکریپت داده شود:
- دیکشنری راه اندازی با دو عملکرد برای مدیریت تکه ها و خطاهای رمزگذاری شده. این توابع توسط برنامهنویس تعریف شدهاند و پس از ارسال به سازنده
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()
برای مشکلاتی که در پیادهسازی کدکها با آن مواجه میشوند، گزارش میشوند. اگر رمزگذاری با موفقیت کامل شود، فراخوانی 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()
داده میشوند.
مجموعه پارامترهای کدک از کدک به کدک دیگر متفاوت است. به عنوان مثال، کدک H.264 ممکن است به یک حباب باینری 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
اگر تکه فقط پس از رمزگشایی یک یا چند تکه قبلی قابل رمزگشایی باشد.
-
همچنین هر تکهای که از رمزگذار ساطع میشود، همانطور که هست برای رمزگشا آماده است. تمام مواردی که در بالا در مورد گزارش خطا و ماهیت ناهمزمان روش های رمزگذار گفته شد برای رمزگشاها نیز صادق است.
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);
}
نکات برنامه نویس
برای مشاهده گزارشهای رسانه و اشکالزدایی WebCodecها، از پنل رسانه در ابزار توسعهدهنده Chrome استفاده کنید.
نسخه ی نمایشی
دمو زیر نشان می دهد که فریم های انیمیشن از یک بوم چگونه هستند:
- با سرعت 25 فریم بر ثانیه در
ReadableStream
توسطMediaStreamTrackProcessor
گرفته شده است - به یک کارگر وب منتقل می شود
- به فرمت ویدیویی H.264 کدگذاری شده است
- دوباره به دنباله ای از فریم های ویدئو رمزگشایی شد
- و با استفاده از
transferControlToOffscreen()
بر روی بوم دوم رندر می شود.
دموهای دیگر
همچنین دموهای دیگر ما را بررسی کنید:
با استفاده از WebCodecs API
تشخیص ویژگی
برای بررسی پشتیبانی WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
به خاطر داشته باشید که WebCodecs API فقط در زمینههای امن موجود است، بنابراین اگر self.isSecureContext
نادرست باشد، شناسایی با شکست مواجه میشود.
بازخورد
تیم Chrome میخواهد در مورد تجربیات شما با WebCodecs API بشنود.
در مورد طراحی API به ما بگویید
آیا چیزی در مورد API وجود دارد که آنطور که انتظار داشتید کار نمی کند؟ یا آیا روش ها یا ویژگی هایی وجود دارد که برای اجرای ایده خود به آنها نیاز دارید؟ سوال یا نظری در مورد مدل امنیتی دارید؟ یک مشکل مشخصات را در مخزن GitHub مربوطه ثبت کنید یا افکار خود را به یک مشکل موجود اضافه کنید.
گزارش مشکل در اجرا
آیا اشکالی در پیاده سازی کروم پیدا کردید؟ یا اجرا با مشخصات متفاوت است؟ یک اشکال را در new.crbug.com ثبت کنید. اطمینان حاصل کنید که تا جایی که می توانید جزئیات، دستورالعمل های ساده برای بازتولید را وارد کنید و Blink>Media>WebCodecs
در کادر Components وارد کنید. Glitch برای به اشتراک گذاری سریع و آسان تکرارها عالی عمل می کند.
پشتیبانی از API را نشان دهید
آیا قصد دارید از WebCodecs API استفاده کنید؟ پشتیبانی عمومی شما به تیم Chrome کمک میکند ویژگیها را اولویتبندی کند و به سایر فروشندگان مرورگر نشان میدهد که چقدر حمایت از آنها ضروری است.
ایمیلهایی را به media-dev@chromium.org ارسال کنید یا با استفاده از هشتگ #WebCodecs
یک توییت به @ChromiumDev ارسال کنید و به ما اطلاع دهید که کجا و چگونه از آن استفاده میکنید.
تصویر قهرمان توسط Denise Jans در Unsplash .