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

جارٍ معالجة مكوّنات بث الفيديو.

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

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

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

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

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

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

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

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

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

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

برامج ترميز الويب وعاملو الويب

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

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

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

أمثلة على استخدام برامج ترميز الويب

الترميز

المسار من لوحة رسم أو 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();

فك الترميز

المسار من الشبكة أو التخزين إلى لوحة رسم أو 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 false.

ملاحظات

يرغب فريق Chrome في معرفة المزيد عن تجاربك مع واجهة برمجة التطبيقات WebCodecs API.

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

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

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

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

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

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

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

الصورة الرئيسية من تصميم دينيس جانس على Unسبل المساعدة