معالجة الفيديو باستخدام 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 هذا القصور من خلال منح المبرمجين طريقة لاستخدام مكوّنات الوسائط المتوفّرة في المتصفّح. على وجه التحديد:

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

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

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

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

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

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

  • يقسّم MediaStreamTrackProcessor مقاطع الوسائط إلى إطارات فردية.
  • تنشئ MediaStreamTrackGenerator مقطعًا صوتيًا أو مرئيًا من سلسلة من اللقطات.

WebCodecs وعمال الويب

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

للمساعدة في ذلك، توفّر واجهة ReadableStream طريقة ملائمة لنقل جميع اللقطات الواردة من مسار وسائط إلى العامل تلقائيًا. على سبيل المثال، يمكن استخدام MediaStreamTrackProcessor للحصول على ReadableStream لمسار بث وسائط وارد من كاميرا الويب. بعد ذلك، يتم نقل البث إلى عامل ويب تتم فيه قراءة اللقطات واحدة تلو الأخرى وإضافتها إلى قائمة الانتظار في 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.
  • كائن إعدادات برنامج الترميز الذي يحتوي على مَعلمات لتدفق الفيديو الناتج. يمكنك تغيير هذه المَعلمات لاحقًا من خلال طلب 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().

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

  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.

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

عرض توضيحي

يوضّح العرض التوضيحي كيفية تنفيذ ما يلي:

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

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

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

استخدام WebCodecs API

رصد الميزات

للتحقّق من توفّر WebCodecs، اتّبِع الخطوات التالية:

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

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

مزيد من المعلومات

إذا كنت جديدًا على WebCodecs، تقدّم أساسيات WebCodecs مقالات مفصّلة تتضمّن العديد من الأمثلة لمساعدتك في التعرّف على المزيد من المعلومات.

الملاحظات

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

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

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

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

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

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

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

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