WebCodecs সঙ্গে ভিডিও প্রক্রিয়াকরণ

ভিডিও স্ট্রিমের উপাদানগুলো নিয়ন্ত্রণ করা।

ইউজিন জেমটসভ
Eugene Zemtsov
ফ্রাঁসোয়া বোফোর্ট
François Beaufort

আধুনিক ওয়েব প্রযুক্তি ভিডিও নিয়ে কাজ করার জন্য প্রচুর উপায় সরবরাহ করে। মিডিয়া স্ট্রিম এপিআই (Media Stream API) , মিডিয়া রেকর্ডিং এপিআই (Media Recording API) , মিডিয়া সোর্স এপিআই (Media Source API) , এবং ওয়েবআরটিসি এপিআই (WebRTC API) মিলে ভিডিও স্ট্রিম রেকর্ড, স্থানান্তর এবং প্লে করার জন্য একটি সমৃদ্ধ টুল সেট তৈরি করে। কিছু নির্দিষ্ট উচ্চ-স্তরের কাজ সমাধান করলেও, এই এপিআইগুলো ওয়েব প্রোগ্রামারদের একটি ভিডিও স্ট্রিমের স্বতন্ত্র উপাদান, যেমন ফ্রেম এবং এনকোড করা ভিডিও বা অডিওর আনমাক্সড খণ্ডাংশ নিয়ে কাজ করতে দেয় না। এই মৌলিক উপাদানগুলোতে নিম্ন-স্তরের অ্যাক্সেস পেতে, ডেভেলপাররা ভিডিও এবং অডিও কোডেকগুলোকে ব্রাউজারে নিয়ে আসার জন্য ওয়েবঅ্যাসেম্বলি (WebAssembly) ব্যবহার করে আসছেন। কিন্তু যেহেতু আধুনিক ব্রাউজারগুলোতে ইতিমধ্যেই বিভিন্ন ধরনের কোডেক অন্তর্ভুক্ত থাকে (যা প্রায়শই হার্ডওয়্যার দ্বারা ত্বরান্বিত হয়), তাই সেগুলোকে ওয়েবঅ্যাসেম্বলি হিসেবে পুনরায় প্যাকেজ করা মানব ও কম্পিউটার সম্পদের অপচয় বলে মনে হয়।

WebCodecs API প্রোগ্রামারদের ব্রাউজারে আগে থেকেই উপস্থিত মিডিয়া কম্পোনেন্টগুলো ব্যবহার করার একটি উপায় দিয়ে এই অদক্ষতা দূর করে। বিশেষত:

  • ভিডিও এবং অডিও ডিকোডার
  • ভিডিও এবং অডিও এনকোডার
  • কাঁচা ভিডিও ফ্রেম
  • ইমেজ ডিকোডার

WebCodecs API এমন ওয়েব অ্যাপ্লিকেশনগুলির জন্য উপযোগী, যেগুলিতে মিডিয়া কন্টেন্ট প্রক্রিয়াকরণের পদ্ধতির উপর সম্পূর্ণ নিয়ন্ত্রণ প্রয়োজন, যেমন ভিডিও এডিটর, ভিডিও কনফারেন্সিং, ভিডিও স্ট্রিমিং ইত্যাদি।

ভিডিও প্রক্রিয়াকরণ কর্মপ্রবাহ

ভিডিও প্রসেসিং-এর কেন্দ্রবিন্দু হলো ফ্রেম। তাই WebCodecs-এর বেশিরভাগ ক্লাসই ফ্রেম গ্রহণ বা তৈরি করে। ভিডিও এনকোডার ফ্রেমকে এনকোড করা খণ্ডে রূপান্তরিত করে। ভিডিও ডিকোডার এর বিপরীত কাজটি করে।

এছাড়াও, VideoFrame একটি CanvasImageSource হওয়ায় এবং এর একটি কনস্ট্রাক্টর থাকায় যা CanvasImageSource গ্রহণ করে, এটি অন্যান্য ওয়েব এপিআই-এর সাথে ভালোভাবে কাজ করে। ফলে এটি drawImage() এবং texImage2D() এর মতো ফাংশনে ব্যবহার করা যায়। এছাড়া এটি ক্যানভাস, বিটম্যাপ, ভিডিও এলিমেন্ট এবং অন্যান্য ভিডিও ফ্রেম থেকেও তৈরি করা যায়।

WebCodecs API, Insertable Streams API- এর সেই ক্লাসগুলির সাথে ভালোভাবে কাজ করে, যেগুলি WebCodecs-কে মিডিয়া স্ট্রিম ট্র্যাকের সাথে সংযুক্ত করে।

  • MediaStreamTrackProcessor মিডিয়া ট্র্যাকগুলোকে স্বতন্ত্র ফ্রেমে বিভক্ত করে।
  • MediaStreamTrackGenerator ফ্রেমের একটি স্ট্রিম থেকে একটি মিডিয়া ট্র্যাক তৈরি করে।

ওয়েবকোডেক এবং ওয়েব ওয়ার্কার

ডিজাইন অনুযায়ী WebCodecs API সমস্ত জটিল কাজ অ্যাসিঙ্ক্রোনাসভাবে এবং মেইন থ্রেডের বাইরে করে থাকে। কিন্তু যেহেতু ফ্রেম এবং চাঙ্ক কলব্যাকগুলো প্রায়শই প্রতি সেকেন্ডে একাধিকবার কল করা হতে পারে, তাই এগুলো মেইন থ্রেডকে ভারাক্রান্ত করতে পারে এবং এর ফলে ওয়েবসাইটটি কম রেসপন্সিভ হয়ে পড়ে। তাই, স্বতন্ত্র ফ্রেম এবং এনকোডেড চাঙ্কগুলোর হ্যান্ডলিং একটি ওয়েব ওয়ার্কারের মধ্যে নিয়ে যাওয়াই শ্রেয়।

এই কাজে সাহায্য করার জন্য, ReadableStream একটি মিডিয়া ট্র্যাক থেকে আসা সমস্ত ফ্রেমকে স্বয়ংক্রিয়ভাবে ওয়ার্কারে স্থানান্তর করার একটি সুবিধাজনক উপায় প্রদান করে। উদাহরণস্বরূপ, ওয়েব ক্যামেরা থেকে আসা একটি মিডিয়া স্ট্রিম ট্র্যাকের জন্য ReadableStream পেতে MediaStreamTrackProcessor ব্যবহার করা যেতে পারে। এরপর স্ট্রিমটি একটি ওয়েব ওয়ার্কারে স্থানান্তরিত হয়, যেখানে ফ্রেমগুলো এক এক করে পড়া হয় এবং একটি VideoEncoder এ সারিবদ্ধ করা হয়।

HTMLCanvasElement.transferControlToOffscreen ব্যবহার করে এমনকি রেন্ডারিংও মূল থ্রেডের বাইরে করা যায়। কিন্তু যদি সমস্ত উচ্চ-স্তরের সরঞ্জামগুলি অসুবিধাজনক বলে মনে হয়, তবে VideoFrame নিজেই স্থানান্তরযোগ্য এবং এটিকে ওয়ার্কারদের মধ্যে সরানো যেতে পারে।

ওয়েবকোডেকস বাস্তবে

এনকোডিং

একটি ক্যানভাস বা ইমেজবিটম্যাপ থেকে নেটওয়ার্ক বা স্টোরেজে যাওয়ার পথ
একটি Canvas বা ImageBitmap থেকে নেটওয়ার্ক বা স্টোরেজে যাওয়ার পথ

সবকিছুর শুরু হয় একটি VideoFrame দিয়ে। ভিডিও ফ্রেম তৈরি করার তিনটি উপায় রয়েছে।

  • ক্যানভাস, ইমেজ বিটম্যাপ বা ভিডিও এলিমেন্টের মতো কোনো ইমেজ সোর্স থেকে।

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • MediaStreamTrack থেকে ফ্রেম টানতে MediaStreamTrackProcessor ব্যবহার করুন।

    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 দুটি জাভাস্ক্রিপ্ট অবজেক্ট দিতে হবে:

  • এনকোড করা চাঙ্ক এবং ত্রুটি পরিচালনার জন্য দুটি ফাংশন সহ একটি ডিকশনারি শুরু করুন। এই ফাংশনগুলো ডেভেলপার-সংজ্ঞায়িত এবং 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 দেখায় যে পূর্ববর্তী এনকোডগুলো শেষ হওয়ার জন্য কিউতে কতগুলো অনুরোধ অপেক্ষা করছে। আর্গুমেন্ট বা মেথড কলের ক্রম যদি 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() কল করে এর প্রমিজের জন্য অপেক্ষা করতে পারেন।

await encoder.flush();

ডিকোডিং

নেটওয়ার্ক বা স্টোরেজ থেকে ক্যানভাস বা ইমেজবিটম্যাপ পর্যন্ত পথ।
নেটওয়ার্ক বা স্টোরেজ থেকে 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);
}

ডেভ টিপস

মিডিয়া লগ দেখতে এবং ওয়েবকোডেক ডিবাগ করতে ক্রোম ডেভটুলস-এর মিডিয়া প্যানেল ব্যবহার করুন।

ওয়েবকোডেক ডিবাগ করার জন্য মিডিয়া প্যানেলের স্ক্রিনশট
ওয়েবকোডেক ডিবাগ করার জন্য ক্রোম ডেভটুলস-এর মিডিয়া প্যানেল।

ডেমো

এই ডেমোতে দেখানো হয়েছে একটি ক্যানভাস থেকে অ্যানিমেশন ফ্রেমগুলো কেমন হয়:

  • MediaStreamTrackProcessor দ্বারা 25fps গতিতে একটি ReadableStream এ ধারণ করা হয়েছে।
  • একজন ওয়েব কর্মীর কাছে স্থানান্তরিত
  • H.264 ভিডিও ফরম্যাটে এনকোড করা হয়েছে
  • পুনরায় ডিকোড করে ভিডিও ফ্রেমের একটি অনুক্রমে পরিণত করা হয়েছে
  • এবং transferControlToOffscreen() ব্যবহার করে দ্বিতীয় ক্যানভাসে রেন্ডার করা হয়েছে।

অন্যান্য ডেমো

আমাদের অন্যান্য ডেমোগুলোও দেখে নিন:

WebCodecs API ব্যবহার করে

বৈশিষ্ট্য সনাক্তকরণ

WebCodecs সমর্থনের জন্য যাচাই করতে:

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

মনে রাখবেন যে WebCodecs API শুধুমাত্র সুরক্ষিত প্রেক্ষাপটে উপলব্ধ, তাই self.isSecureContext মিথ্যা হলে সনাক্তকরণ ব্যর্থ হবে।

আরও জানুন

আপনি যদি WebCodecs-এ নতুন হন, তবে WebCodecs Fundamentals-এ অনেক উদাহরণসহ বিশদ প্রবন্ধ রয়েছে যা আপনাকে আরও জানতে সাহায্য করবে।

প্রতিক্রিয়া

ক্রোম টিম ওয়েবকোডেকস এপিআই (WebCodecs API) নিয়ে আপনার অভিজ্ঞতা সম্পর্কে জানতে আগ্রহী।

এপিআই ডিজাইন সম্পর্কে আমাদের বলুন

এপিআই-এর এমন কিছু কি আছে যা আপনার প্রত্যাশা অনুযায়ী কাজ করছে না? অথবা আপনার ধারণাটি বাস্তবায়নের জন্য প্রয়োজনীয় কোনো মেথড বা প্রপার্টি কি অনুপস্থিত? নিরাপত্তা মডেল সম্পর্কে আপনার কোনো প্রশ্ন বা মন্তব্য আছে? সংশ্লিষ্ট গিটহাব রিপোজিটরিতে একটি স্পেক ইস্যু ফাইল করুন, অথবা বিদ্যমান কোনো ইস্যুতে আপনার মতামত যোগ করুন।

বাস্তবায়নে একটি সমস্যা রিপোর্ট করুন

আপনি কি ক্রোমের বাস্তবায়নে কোনো বাগ খুঁজে পেয়েছেন? অথবা এর বাস্তবায়ন কি স্পেসিফিকেশন থেকে ভিন্ন? new.crbug.com- এ একটি বাগ রিপোর্ট করুন। যতটা সম্ভব বিস্তারিত তথ্য, বাগটি পুনরায় তৈরি করার সহজ নির্দেশাবলী অন্তর্ভুক্ত করতে ভুলবেন না এবং Components বক্সে Blink>Media>WebCodecs লিখুন।

এপিআই-এর প্রতি সমর্থন দেখান

আপনি কি WebCodecs API ব্যবহার করার পরিকল্পনা করছেন? আপনার প্রকাশ্য সমর্থন Chrome টিমকে ফিচারগুলোকে অগ্রাধিকার দিতে সাহায্য করে এবং অন্যান্য ব্রাউজার নির্মাতাদের দেখায় যে এটিকে সমর্থন করা কতটা জরুরি।

#WebCodecs হ্যাশট্যাগটি ব্যবহার করে media-dev@chromium.org- এ ইমেল করুন অথবা @ChromiumDev- কে টুইট করুন এবং আমাদের জানান যে আপনি এটি কোথায় এবং কীভাবে ব্যবহার করছেন।