پردازش ویدیو با WebCodecs

دستکاری اجزای جریان ویدئو.

یوجین زمتسف
Eugene Zemtsov
فرانسوا بوفور
François Beaufort

فناوری‌های مدرن وب، روش‌های فراوانی برای کار با ویدیو ارائه می‌دهند. APIهای Media Stream ، Media Recording API ، Media Source API و WebRTC API ، مجموعه‌ای غنی از ابزارها را برای ضبط، انتقال و پخش جریان‌های ویدیویی فراهم می‌کنند. این APIها ضمن حل برخی وظایف سطح بالا، به برنامه‌نویسان وب اجازه نمی‌دهند با اجزای منفرد یک جریان ویدیویی مانند فریم‌ها و تکه‌های unmux شده ویدیو یا صدا رمزگذاری شده کار کنند. برای دسترسی سطح پایین به این اجزای اساسی، توسعه‌دهندگان از WebAssembly برای آوردن کدک‌های ویدیویی و صوتی به مرورگر استفاده کرده‌اند. اما با توجه به اینکه مرورگرهای مدرن از قبل با انواع کدک‌ها (که اغلب توسط سخت‌افزار شتاب می‌گیرند) عرضه می‌شوند، بسته‌بندی مجدد آنها به عنوان WebAssembly به نظر اتلاف منابع انسانی و رایانه‌ای است.

API وب‌کدکس با ارائه راهی به برنامه‌نویسان برای استفاده از اجزای رسانه‌ای که از قبل در مرورگر وجود دارند، این ناکارآمدی را از بین می‌برد. به طور خاص:

  • رمزگشاهای ویدیو و صدا
  • انکودرهای ویدیو و صدا
  • فریم‌های ویدیویی خام
  • رمزگشاهای تصویر

رابط برنامه‌نویسی کاربردی وب کدکس (WebCodecs API) برای برنامه‌های تحت وب که نیاز به کنترل کامل بر نحوه پردازش محتوای رسانه دارند، مانند ویرایشگرهای ویدیو، کنفرانس‌های ویدیویی، پخش ویدیو و غیره، مفید است.

گردش کار پردازش ویدئو

فریم‌ها محور پردازش ویدیو هستند. بنابراین در WebCodecs اکثر کلاس‌ها یا فریم‌ها را مصرف می‌کنند یا تولید می‌کنند. رمزگذارهای ویدیو فریم‌ها را به تکه‌های رمزگذاری شده تبدیل می‌کنند. رمزگشاهای ویدیو برعکس عمل می‌کنند.

همچنین VideoFrame به خوبی با سایر APIهای وب سازگار است، زیرا یک CanvasImageSource است و سازنده‌ای دارد که CanvasImageSource می‌پذیرد. بنابراین می‌تواند در توابعی مانند drawImage() و texImage2D() استفاده شود. همچنین می‌تواند از canvases، bitmaps، عناصر ویدیویی و سایر فریم‌های ویدیویی ساخته شود.

API مربوط به WebCodecs به خوبی با کلاس‌های موجود در Insertable Streams API که WebCodecs را به مسیرهای پخش رسانه متصل می‌کنند، هماهنگ عمل می‌کند.

  • MediaStreamTrackProcessor آهنگ‌های رسانه‌ای را به فریم‌های جداگانه تجزیه می‌کند.
  • MediaStreamTrackGenerator یک مسیر رسانه‌ای از جریانی از فریم‌ها ایجاد می‌کند.

وب کدک‌ها و وب ورکرها

طبق طراحی، WebCodecs API تمام کارهای سنگین را به صورت غیرهمزمان و خارج از نخ اصلی انجام می‌دهد. اما از آنجایی که فراخوانی‌های frame و chunk اغلب می‌توانند چندین بار در ثانیه فراخوانی شوند، ممکن است thread اصلی را شلوغ کنند و در نتیجه وب‌سایت را کمتر پاسخگو کنند. بنابراین ترجیح داده می‌شود که مدیریت فریم‌ها و تکه‌های کدگذاری شده را به صورت جداگانه به یک web worker منتقل کنید.

برای کمک به این امر، ReadableStream روشی مناسب برای انتقال خودکار تمام فریم‌های دریافتی از یک مسیر رسانه‌ای به worker فراهم می‌کند. به عنوان مثال، MediaStreamTrackProcessor می‌تواند برای دریافت ReadableStream برای یک مسیر جریان رسانه‌ای که از دوربین وب می‌آید، استفاده شود. پس از آن، جریان به یک worker وب منتقل می‌شود که در آن فریم‌ها یکی یکی خوانده می‌شوند و در یک VideoEncoder صف می‌شوند.

با HTMLCanvasElement.transferControlToOffscreen حتی رندر کردن هم می‌تواند خارج از نخ اصلی انجام شود. اما اگر تمام ابزارهای سطح بالا نامناسب باشند، خود VideoFrame قابل انتقال است و می‌توان آن را بین workerها جابجا کرد.

وب‌کدک‌ها در عمل

رمزگذاری

مسیر از یک 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);
    

فرقی نمی‌کند که فریم‌ها از کجا می‌آیند، آن‌ها را می‌توان با استفاده از VideoEncoder در اشیاء EncodedVideoChunk کدگذاری کرد.

قبل از کدگذاری، VideoEncoder باید دو شیء جاوا اسکریپت دریافت کند:

  • دیکشنری Init با دو تابع برای مدیریت تکه‌های کدگذاری شده و خطاها. این توابع توسط توسعه‌دهنده تعریف شده‌اند و پس از ارسال به سازنده‌ی VideoEncoder قابل تغییر نیستند.
  • شیء پیکربندی انکودر، که شامل پارامترهایی برای جریان ویدیوی خروجی است. می‌توانید این پارامترها را بعداً با فراخوانی configure() تغییر دهید.

اگر پیکربندی توسط مرورگر پشتیبانی نشود، متد configure() NotSupportedError را صادر می‌کند. توصیه می‌شود متد استاتیک VideoEncoder.isConfigSupported() را همراه با پیکربندی فراخوانی کنید تا از قبل بررسی کنید که آیا پیکربندی پشتیبانی می‌شود یا خیر و منتظر promise آن باشید.

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 نشان می‌دهد که چه تعداد درخواست در صف منتظر اتمام رمزگذاری‌های قبلی هستند. خطاها یا با ارسال یک استثنا بلافاصله گزارش می‌شوند، در صورتی که آرگومان‌ها یا ترتیب فراخوانی متدها، قرارداد API را نقض کند، یا با فراخوانی تابع فراخوانی 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() را فراخوانی کنید و منتظر promise آن باشید.

await encoder.flush();

رمزگشایی

مسیر از شبکه یا فضای ذخیره‌سازی به یک Canvas یا یک ImageBitmap.
مسیر از شبکه یا فضای ذخیره‌سازی به یک Canvas یا یک ImageBitmap .

راه‌اندازی یک VideoDecoder مشابه کاری است که برای VideoEncoder انجام شده است: دو تابع هنگام ایجاد decoder ارسال می‌شوند و پارامترهای codec به 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() را فراخوانی کنید تا حافظه‌ی زیرین را قبل از اینکه garbage collector به آن برسد، آزاد کند. این کار باعث کاهش میانگین حافظه‌ی مورد استفاده توسط برنامه‌ی وب می‌شود.

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);
}

نکات توسعه

برای مشاهده گزارش‌های رسانه‌ای و اشکال‌زدایی WebCodecs از پنل رسانه در Chrome DevTools استفاده کنید.

تصویر پنل رسانه برای اشکال‌زدایی WebCodecs
پنل رسانه در ابزارهای توسعه کروم برای اشکال‌زدایی از WebCodecs.

نسخه آزمایشی

این دمو نشان می‌دهد که فریم‌های انیمیشن از یک بوم چگونه هستند:

  • با سرعت ۲۵ فریم در ثانیه توسط MediaStreamTrackProcessor در یک ReadableStream ضبط شده است
  • به یک کارگر وب منتقل شد
  • کدگذاری شده در قالب ویدیویی H.264
  • دوباره به دنباله ای از فریم های ویدیویی رمزگشایی شد
  • و با استفاده از transferControlToOffscreen() روی بوم دوم رندر شد

نسخه‌های نمایشی دیگر

همچنین از سایر دموهای ما دیدن کنید:

استفاده از API وب‌کدکس

تشخیص ویژگی

برای بررسی پشتیبانی WebCodecs:

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

به خاطر داشته باشید که API وب‌کدکس فقط در زمینه‌های امن در دسترس است، بنابراین اگر self.isSecureContext برابر با false باشد، تشخیص با شکست مواجه خواهد شد.

بیشتر بدانید

اگر در زمینه WebCodecs تازه‌کار هستید، WebCodecs Fundamentals مقالات عمیقی را با مثال‌های فراوان ارائه می‌دهد تا به شما در یادگیری بیشتر کمک کند.

بازخورد

تیم کروم می‌خواهد از تجربیات شما در استفاده از API وب‌کدکس مطلع شود.

در مورد طراحی API به ما بگویید

آیا چیزی در مورد API وجود دارد که آنطور که انتظار داشتید کار نمی‌کند؟ یا متدها یا ویژگی‌هایی وجود ندارند که برای پیاده‌سازی ایده خود به آنها نیاز دارید؟ در مورد مدل امنیتی سؤال یا نظری دارید؟ یک مشکل مربوط به مشخصات را در مخزن مربوطه GitHub ثبت کنید، یا نظرات خود را به یک مشکل موجود اضافه کنید.

گزارش مشکل در پیاده‌سازی

آیا در پیاده‌سازی کروم اشکالی پیدا کردید؟ یا پیاده‌سازی با مشخصات متفاوت است؟ یک اشکال را در new.crbug.com ثبت کنید. حتماً تا حد امکان جزئیات، دستورالعمل‌های ساده برای بازتولید را ذکر کنید و Blink>Media>WebCodecs را در کادر Components وارد کنید.

نمایش پشتیبانی از API

آیا قصد دارید از API وب‌کدکس استفاده کنید؟ حمایت عمومی شما به تیم کروم کمک می‌کند تا ویژگی‌ها را اولویت‌بندی کند و به سایر فروشندگان مرورگر نشان می‌دهد که پشتیبانی از آنها چقدر حیاتی است.

به آدرس media-dev@chromium.org ایمیل بفرستید یا با استفاده از هشتگ #WebCodecs توییتی به آدرس @ChromiumDev ارسال کنید و به ما اطلاع دهید که کجا و چگونه از آن استفاده می‌کنید.

تصویر قهرمان از دنیس جانس در Unsplash .