معالجة الفيديو باستخدام 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();

فك التشفير

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

يشبه إعداد VideoDecoder ما تمّ فعله في VideoEncoder: يتمّ تمرير وظيفتَين عند إنشاء وحدة الترميز/فك الترميز، ويتمّ منح مَعلمات configure() ل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.

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

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

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

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

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

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

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

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