التلاعب بمكوّنات بث الفيديو
توفّر تقنيات الويب الحديثة طرقًا عديدة للتعامل مع الفيديو. توفّر واجهة برمجة التطبيقات Media Stream API وواجهة برمجة التطبيقات Media Recording API وواجهة برمجة التطبيقات Media Source API وواجهة برمجة التطبيقات WebRTC API مجموعة أدوات شاملة لتسجيل فيديوهات البث ونقلها وتشغيلها. أثناء حلّ مهام معيّنة رفيعة المستوى، لا تسمح واجهات برمجة التطبيقات هذه لمبرمجي الويب بالتعامل مع المكوّنات الفردية لتدفق الفيديو، مثل اللقطات وأجزاء الفيديو أو الصوت المرمّز غير المدمج. للحصول على إذن وصول منخفض المستوى إلى هذه المكوّنات الأساسية، كان المطوّرون يستخدمون WebAssembly لنقل برامج ترميز الفيديو والصوت إلى المتصفح. ولكن بما أنّ المتصفحات الحديثة تتضمّن مجموعة متنوعة من برامج الترميز (التي غالبًا ما يتم تسريعها بواسطة الأجهزة)، فإنّ إعادة تجميعها على شكل WebAssembly تبدو مضيعة لموارد الإنسان والكمبيوتر.
تزيل WebCodecs API هذا القصور من خلال منح المبرمجين طريقة لاستخدام مكوّنات الوسائط المتوفّرة في المتصفّح. على وجه التحديد:
- برامج فك ترميز الفيديو والصوت
- برامج ترميز الفيديو والصوت
- إطارات الفيديو الأولية
- أدوات فك ترميز الصور
تُعدّ WebCodecs API مفيدة لتطبيقات الويب التي تتطلّب تحكّمًا كاملاً في طريقة معالجة محتوى الوسائط، مثل برامج تعديل الفيديوهات ومؤتمرات الفيديو وبث الفيديو وما إلى ذلك.
سير عمل معالجة الفيديو
تشكّل الإطارات الجزء الأساسي في معالجة الفيديو. وبالتالي، تستهلك معظم الفئات في WebCodecs إطارات أو تنتجها. تحوّل برامج ترميز الفيديو اللقطات إلى أجزاء مرمّزة. وتقوم برامج فك ترميز الفيديو بالعكس.
يتوافق VideoFrame
أيضًا مع واجهات برمجة تطبيقات الويب الأخرى من خلال كونه CanvasImageSource
ويتضمّن دالة إنشاء تقبل CanvasImageSource
.
لذا يمكن استخدامها في دوال مثل drawImage()
وtexImage2D()
. يمكن أيضًا إنشاء هذه الفيديوهات من لوحات الرسم والصور النقطية وعناصر الفيديو وإطارات الفيديو الأخرى.
تعمل واجهة برمجة التطبيقات WebCodecs بشكل جيد جنبًا إلى جنب مع الفئات من Insertable Streams API التي تربط WebCodecs بمقاطع بث الوسائط.
MediaStreamTrackProcessor
يقسّم مقاطع الوسائط إلى إطارات فردية.- تنشئ
MediaStreamTrackGenerator
مقطع وسائط من سلسلة من اللقطات.
WebCodecs وWeb Workers
تم تصميم واجهة برمجة التطبيقات WebCodecs لتنفيذ جميع العمليات المعقّدة بشكل غير متزامن وخارج سلسلة التعليمات الرئيسية. ولكن بما أنّ عمليات معاودة الاتصال بالإطارات والتقسيمات يمكن أن تتم عدة مرات في الثانية، قد تؤدي إلى تشويش سلسلة التعليمات الرئيسية وبالتالي جعل الموقع الإلكتروني أقل استجابة. لذلك، من الأفضل نقل عملية معالجة اللقطات الفردية والأجزاء المشفرة إلى Web Worker.
للمساعدة في ذلك، توفّر واجهة ReadableStream طريقة ملائمة لنقل جميع اللقطات الواردة من مسار وسائط إلى العامل تلقائيًا. على سبيل المثال، يمكن استخدام MediaStreamTrackProcessor
للحصول على
ReadableStream
لمسار بث وسائط وارد من كاميرا الويب. بعد ذلك، يتم نقل البث إلى عامل ويب تتم فيه قراءة الإطارات واحدًا تلو الآخر ووضعها في قائمة انتظار ضمن VideoEncoder
.
باستخدام HTMLCanvasElement.transferControlToOffscreen
، يمكن حتى إجراء عملية العرض خارج سلسلة التعليمات الرئيسية. ولكن إذا تبيّن أنّ جميع الأدوات المتقدّمة غير ملائمة، يمكن نقل 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:
- تهيئة القاموس باستخدام دالتَين لمعالجة الأجزاء المرمّزة والأخطاء هذه الدوال يحدّدها المطوّر ولا يمكن تغييرها بعد تمريرها إلى الدالة الإنشائية
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
عدد الطلبات التي تنتظر في قائمة الانتظار إلى أن تنتهي عمليات الترميز السابقة.
يتم الإبلاغ عن الأخطاء إما عن طريق طرح استثناء على الفور، في حال كانت الوسيطات أو ترتيب طلبات الطرق ينتهك عقد واجهة برمجة التطبيقات، أو عن طريق استدعاء 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();
فك الترميز

Canvas
أو ImageBitmap
يشبه إعداد VideoDecoder
ما تم إجراؤه في VideoEncoder
: يتم تمرير وظيفتَين عند إنشاء برنامج الترميز، ويتم تقديم مَعلمات برنامج الترميز إلى configure()
.
تختلف مجموعة مَعلمات برامج الترميز من برنامج إلى آخر. على سبيل المثال، قد يحتاج برنامج الترميز H.264 إلى ثنائي كبير من 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
- تم نقلها إلى عامل ويب
- مرمّز بتنسيق فيديو H.264
- يتم فك ترميزها مرة أخرى إلى سلسلة من لقطات الفيديو
- ويتم عرضها على لوحة الرسم الثانية باستخدام
transferControlToOffscreen()
العروض التوضيحية الأخرى
يمكنك أيضًا الاطّلاع على العروض التوضيحية الأخرى:
استخدام WebCodecs API
رصد الميزات
للتحقّق من توفّر WebCodecs، اتّبِع الخطوات التالية:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
يُرجى العِلم أنّ WebCodecs API لا تتوفّر إلا في السياقات الآمنة،
لذا سيتعذّر الرصد إذا كانت قيمة self.isSecureContext
هي false.
الملاحظات
يريد فريق Chrome معرفة رأيك في تجربة استخدام WebCodecs API.
أخبِرنا عن تصميم واجهة برمجة التطبيقات
هل هناك أي شيء في واجهة برمجة التطبيقات لا يعمل على النحو المتوقع؟ أو هل هناك طرق أو سمات ناقصة تحتاج إلى تنفيذها لتحقيق فكرتك؟ هل لديك سؤال أو تعليق حول نموذج الأمان؟ يمكنك الإبلاغ عن مشكلة في المواصفات في مستودع GitHub ذي الصلة، أو إضافة أفكارك إلى مشكلة حالية.
الإبلاغ عن مشكلة في عملية التنفيذ
هل عثرت على خطأ في تنفيذ Chrome؟ أو هل يختلف التنفيذ عن المواصفات؟ يمكنك الإبلاغ عن الخطأ على new.crbug.com.
احرص على تضمين أكبر قدر ممكن من التفاصيل، وتعليمات بسيطة لإعادة إنتاج الخطأ، وأدخِل Blink>Media>WebCodecs
في المربّع المكوّنات.
إظهار الدعم لواجهة برمجة التطبيقات
هل تخطّط لاستخدام WebCodecs API؟ يساعد دعمك العلني فريق Chrome في تحديد أولويات الميزات، ويوضّح لمورّدي المتصفّحات الآخرين مدى أهمية توفيرها.
يمكنك إرسال رسائل إلكترونية إلى media-dev@chromium.org أو تغريدة إلى @ChromiumDev باستخدام الهاشتاغ #WebCodecs
وإخبارنا بمكان استخدامك له وكيفية استخدامه.
الصورة الرئيسية من Denise Jans على Unsplash