معالجة الفيديو باستخدام WebCodecs

التلاعب بمكونات بث الفيديو

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

توفّر تكنولوجيات الويب الحديثة طرقًا متعدّدة للعمل مع الفيديو. تُشكّل Media Stream API وMedia Recording API وMedia Source API وWebRTC API مجموعة أدوات غنية لتسجيل أحداث البث ونقل بياناتها وتشغيلها. أثناء حل بعض المهام عالية المستوى، لا تسمح واجهات برمجة التطبيقات هذه لمبرمجي الويب بالعمل مع المكونات الفردية لبث الفيديو مثل الإطارات والأجزاء غير المختلطة من الفيديو أو الصوت المُرمّز. وللحصول على إمكانية وصول منخفضة المستوى إلى هذه المكوّنات الأساسية، يستخدم المطوّرون WebAssembly لجلب برامج ترميز الفيديو والصوت إلى المتصفّح. ولكن بما أنّه توفّر في المتصفحات الحديثة مجموعة متنوعة من برامج الترميز (التي يتم تسريعها في أغلب الأحيان باستخدام الأجهزة)، فإنّ إعادة تعبئتها كبرامج WebAssembly تبدو وكأنها مضيعة لوقت الموظفين وموارد أجهزة الكمبيوتر.

تقضي واجهة برمجة التطبيقات WebCodecs API على هذه المشكلة من خلال منح المبرمجين طريقة لاستخدام مكوّنات الوسائط المتوفرة حاليًا في browser. وهذه القيود تحديدًا هي كالآتي:

  • برامج فك ترميز الفيديو والصوت
  • برامج ترميز الفيديو والصوت
  • لقطات الفيديو الأولية
  • برامج فك ترميز الصور

تُعد واجهة برمجة التطبيقات WebCodecs API مفيدة لتطبيقات الويب التي تتطلب تحكُّمًا كاملاً في طريقة معالجة محتوى الوسائط، مثل أدوات تحرير الفيديوهات واجتماعات الفيديو وبثها وما إلى ذلك.

سير عمل معالجة الفيديو

الإطارات هي العنصر الرئيسي في معالجة الفيديو. وبالتالي، في WebCodecs، تستهلك معظم الفئات اللقطات أو تنتجها. تعمل برامج ترميز الفيديو على تحويل اللقطات إلى ملفَّات مجزّأة ومرمّزة. تُجري برامج ترميز الفيديوهات العكس.

يتوافق VideoFrame أيضًا مع واجهات برمجة تطبيقات الويب الأخرى من خلال كونه CanvasImageSource وامتلاك مُنشئ يقبل CanvasImageSource. وبالتالي، يمكن استخدامه في دوال مثل drawImage() وtexImage2D(). ويمكن أيضًا إنشاؤه من لوحات وصور نقطية وعناصر فيديو وإطارات فيديو أخرى.

تعمل WebCodecs API بشكل جيد مع الفئات من Insertable Streams API التي تربط WebCodecs بـ مقاطع بث الوسائط.

  • MediaStreamTrackProcessor تقسم مقاطع الوسائط إلى لقطات فردية.
  • تنشئ ميزة "MediaStreamTrackGenerator" مسار وسائط من سلسلة من الإطارات.

WebCodecs وWeb Workers

بتصميمها، تُجري WebCodecs API جميع الإجراءات المُهمّة بشكل غير متزامن وبعيدًا عن سلسلة التعليمات الرئيسية. ولكن بما أنّه يمكن استدعاء عمليات الاستدعاء للإطارات والقطع غالبًا عدة مرات في الثانية، قد تؤدي إلى تشويش سلسلة المهام الرئيسية وبالتالي تقليل سرعة استجابة الموقع الإلكتروني. لذلك، من الأفضل نقل معالجة اللقطات الفردية والقطع المشفَّرة إلى عامل ويب.

للمساعدة في ذلك، توفّر ReadableStream طريقة ملائمة لنقل جميع الإطارات الواردة من مسار وسائط إلى العامل تلقائيًا. على سبيل المثال، يمكن استخدام MediaStreamTrackProcessor للحصول على ReadableStream لمقطع صوتي في بث الوسائط القادم من كاميرا الويب. بعد ذلك، يتم نقل البث إلى Web Worker حيث تتم قراءة اللقطات الواحدة تلو الأخرى ووضعها في قائمة الانتظار في VideoEncoder.

باستخدام HTMLCanvasElement.transferControlToOffscreen، يمكن أيضًا تنفيذ العرض خارج سلسلة التعليمات الرئيسية. ولكن إذا تبيّن أنّ جميع الأدوات العالية المستوى غير ملائمة، يمكن نقل VideoFrame نفسها وقد تتم إزالتها من أحد العمال ونقلها إلى آخر.

استخدام WebCodecs

الترميز

المسار من Canvas أو ImageBitmap إلى الشبكة أو إلى مساحة التخزين
المسار من 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 constructor.
  • عنصر إعدادات برنامج الترميز الذي يحتوي على مَعلمات لبث الفيديو الناتج يمكنك تغيير هذه المَعلمات لاحقًا من خلال الاتصال بالرقم 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();

فك التشفير

المسار من الشبكة أو التخزين إلى لوحة رسم أو ImageBitmap.
المسار من الشبكة أو مساحة التخزين إلى Canvas أو ImageBitmap

يشبه إعداد VideoDecoder ما تم تنفيذه في VideoEncoder: يتمّ ضبط دالتَين عند إنشاء برنامج فك الترميز، وتُمنح مَعلمات برنامج الترميز إلى configure().

تختلف مجموعة معلَمات برنامج الترميز من برنامج ترميز إلى آخر. على سبيل المثال، قد يحتاج برنامج ترميز H.264 إلى ملف ثنائي كبير لترميز AVCC، ما لم يتم ترميزه بما يُعرف باسم "تنسيق الملحق ب" (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()) بسرعة. في المثال أدناه، لا تضيف سوى إطار إلى قائمة الانتظار للإطارات الجاهزة للعرض. تتم عملية التقديم بشكل منفصل، وتتألف من خطوتَين:

  1. في انتظار الوقت المناسب لعرض اللقطة.
  2. رسم الإطار على اللوحة

بعد أن يصبح الإطار غير مطلوب، يمكنك استدعاء 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.

لقطة شاشة للوحة الوسائط لتصحيح أخطاء WebCodecs
لوحة الوسائط في "أدوات مطوري البرامج في Chrome" لتصحيح أخطاء WebCodecs.

عرض توضيحي

يوضّح المقطع التجريبي أدناه كيفية إنشاء إطارات الصور المتحركة من لوحة:

  • تم التقاطه بمعدّل 25 لقطة في الثانية في ReadableStream بواسطة MediaStreamTrackProcessor
  • تم نقلها إلى عامل على الويب
  • تم ترميزها إلى تنسيق فيديو H.264
  • يتم فك ترميزه مرة أخرى إلى سلسلة من لقطات الفيديو
  • ويتم عرضها على اللوحة الثانية باستخدام transferControlToOffscreen()

عروض توضيحية أخرى

يمكنك أيضًا الاطّلاع على العروض التوضيحية الأخرى:

استخدام WebCodecs API

رصد الميزات

للتحقّق من توفّر WebCodecs:

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

يُرجى العِلم أنّ WebCodecs API لا تتوفّر إلّا في السياقات الآمنة، لذلك سيتعذّر رصد المحتوى إذا كان self.isSecureContext غير صحيح.

ملاحظات

يريد فريق Chrome معرفة تجاربك مع WebCodecs API.

أخبِرنا عن تصميم واجهة برمجة التطبيقات.

هل هناك مشكلة في واجهة برمجة التطبيقات لا تعمل على النحو المتوقّع؟ أو هل هناك طرق أو خصائص مفقودة تحتاجها لتنفيذ فكرتك؟ هل لديك سؤال أو تعليق بشأن نموذج الأمان؟ يُرجى الإبلاغ عن مشكلة في المواصفات بشأن مستودع GitHub المقابل، أو إضافة آرائك إلى مشكلة حالية.

الإبلاغ عن مشكلة في التنفيذ

هل رصدت خطأ في عملية تنفيذ Chrome؟ أم أن التنفيذ يختلف عن المواصفات؟ يمكنك الإبلاغ عن خلل على الرابط new.crbug.com. احرص على تضمين أكبر قدر ممكن من التفاصيل وتعليمات بسيطة لإعادة تنفيذ الخطوات التي أدت إلى حدوث الخلل، وأدخِل Blink>Media>WebCodecs في مربّع المكوّنات. يُعدّ تطبيق Glitch مثاليًا لمشاركة عمليات إعادة الإنتاج بسرعة وسهولة.

إظهار الدعم لواجهة برمجة التطبيقات

هل تخطّط لاستخدام واجهة برمجة التطبيقات WebCodecs API؟ يساعد دعمك العلني فريق Chrome في تحديد أولويات الميزات ويُظهر لموفّري المتصفّحات الآخرين مدى أهمية دعمها.

أرسِل رسائل إلكترونية إلى media-dev@chromium.org أو أرسِل تغريدة إلى ‎@ChromiumDev باستخدام الهاشتاغ #WebCodecs وأطلِعنا على مكان استخدامك للميزة وطريقة استخدامك لها.

الصورة الرئيسية من تأليف دينيس جانز على Unsplash.