WebCodecs के साथ वीडियो प्रोसेस करना

वीडियो स्ट्रीम के कॉम्पोनेंट में बदलाव करना.

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

आधुनिक वेब टेक्नोलॉजी की मदद से, वीडियो के साथ काम करने के कई तरीके मिलते हैं. Media Stream API, Media Recording API, Media Source API, और WebRTC API, वीडियो स्ट्रीम को रिकॉर्ड करने, ट्रांसफ़र करने, और चलाने के लिए, एक बेहतर टूल सेट के तौर पर काम करते हैं. कुछ हाई-लेवल टास्क को हल करते समय, ये एपीआई वेब प्रोग्रामर को वीडियो स्ट्रीम के अलग-अलग कॉम्पोनेंट के साथ काम करने की अनुमति नहीं देते. जैसे, एन्कोड किए गए वीडियो या ऑडियो के फ़्रेम और अनमक्स किए गए हिस्से. इन बुनियादी कॉम्पोनेंट का लो-लेवल ऐक्सेस पाने के लिए, डेवलपर ब्राउज़र में वीडियो और ऑडियो कोडेक लाने के लिए, WebAssembly का इस्तेमाल कर रहे हैं. हालांकि, यह देखते हुए कि आधुनिक ब्राउज़र पहले से ही कई तरह के कोडेक के साथ शिप होते हैं (जिन्हें अक्सर हार्डवेयर की मदद से तेज़ किया जाता है), उन्हें वेब असेंबली के तौर पर फिर से पैकेज करना, मानव और कंप्यूटर संसाधनों की बर्बादी है.

WebCodecs API, प्रोग्रामर को ब्राउज़र में पहले से मौजूद मीडिया कॉम्पोनेंट का इस्तेमाल करने का तरीका देकर, इस समस्या को हल करता है. खास तौर पर:

  • वीडियो और ऑडियो डिकोडर
  • वीडियो और ऑडियो एन्कोडर
  • रॉ वीडियो फ़्रेम
  • इमेज डीकोडर

WebCodecs API उन वेब ऐप्लिकेशन के लिए फ़ायदेमंद है जिन्हें मीडिया कॉन्टेंट को प्रोसेस करने के तरीके पर पूरा कंट्रोल चाहिए. जैसे, वीडियो एडिटर, वीडियो कॉन्फ़्रेंसिंग, वीडियो स्ट्रीमिंग वगैरह.

वीडियो प्रोसेस करने का वर्कफ़्लो

वीडियो प्रोसेस करने के लिए फ़्रेम, सेंटरपीस की तरह काम करते हैं. इसलिए, WebCodecs में ज़्यादातर क्लास, फ़्रेम का इस्तेमाल करती हैं या उन्हें जनरेट करती हैं. वीडियो एन्कोडर, फ़्रेम को कोड में बदले गए चंक्स में बदल देते हैं. हालांकि, वीडियो डिकोडर इसके उलट होते हैं.

इसके अलावा, CanvasImageSource होने और CanvasImageSource को स्वीकार करने वाला constructor होने की वजह से, VideoFrame अन्य वेब एपीआई के साथ बेहतर तरीके से काम करता है. इसलिए, इसका इस्तेमाल drawImage() औरtexImage2D() जैसे फ़ंक्शन में किया जा सकता है. साथ ही, इसे कैनवस, बिटमैप, वीडियो एलिमेंट, और दूसरे वीडियो फ़्रेम से भी बनाया जा सकता है.

WebCodecs API, Insertable Streams API की क्लास के साथ मिलकर बेहतर तरीके से काम करता है. ये क्लास, WebCodecs को मीडिया स्ट्रीम ट्रैक से कनेक्ट करती हैं.

  • MediaStreamTrackProcessor, मीडिया ट्रैक को अलग-अलग फ़्रेम में बांटता है.
  • MediaStreamTrackGenerator, फ़्रेम की स्ट्रीम से मीडिया ट्रैक बनाता है.

WebCodecs और वेब वर्कर

डिज़ाइन के हिसाब से, WebCodecs API सभी ज़रूरी काम, एसिंक्रोनस तरीके से और मुख्य थ्रेड से बाहर करता है. हालांकि, फ़्रेम और हिस्से कॉलबैक को अक्सर एक सेकंड में कई बार कहा जा सकता है. इसलिए, वे मुख्य थ्रेड को अव्यवस्थित कर सकते हैं और वेबसाइट को कम रिस्पॉन्सिव बना सकते हैं. इसलिए, अलग-अलग फ़्रेम और कोड में बदले गए हिस्सों को मैनेज करने की प्रोसेस को वेब वर्कर्स में ले जाना बेहतर होता है.

इसमें मदद करने के लिए, ReadableStream एक आसान तरीका उपलब्ध कराता है. इसकी मदद से, मीडिया ट्रैक से आने वाले सभी फ़्रेम, वर्कर्स में अपने-आप ट्रांसफ़र हो जाते हैं. उदाहरण के लिए, वेब कैमरे से आने वाले मीडिया स्ट्रीम ट्रैक के लिए, ReadableStream को पाने के लिए MediaStreamTrackProcessor का इस्तेमाल किया जा सकता है. इसके बाद, स्ट्रीम को वेब वर्कर्स पर ट्रांसफ़र किया जाता है. यहां फ़्रेम को एक-एक करके पढ़ा जाता है और VideoEncoder में लाइन में लगाया जाता है.

HTMLCanvasElement.transferControlToOffscreen की मदद से, मुख्य थ्रेड के अलावा भी रेंडरिंग की जा सकती है. हालांकि, अगर सभी बेहतर टूल काम के नहीं लगते, तो VideoFrame को ट्रांसफ़र किया जा सकता है और इसे एक से ज़्यादा वर्कर के बीच ट्रांसफ़र किया जा सकता है.

कार्रवाई में WebCodecs

एन्कोडिंग

कैनवस या ImageBitmap से नेटवर्क या स्टोरेज तक का पाथ
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 को दो 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() callback को कॉल करके भी गड़बड़ियों की शिकायत की जा सकती है. अगर कोडिंग पूरी हो जाती है, तो output() callback को आर्ग्युमेंट के तौर पर, कोड में बदले गए नए चंक के साथ कॉल किया जाता है. यहां एक और अहम जानकारी यह है कि जब फ़्रेम की ज़रूरत न हो, तब उन्हें 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 के बाइनरी ब्लॉब की ज़रूरत पड़ सकती है. हालांकि, ऐसा तब तक नहीं होगा, जब तक इसे 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);
}

डेवलपर के लिए सलाह

मीडिया लॉग देखने और WebCodecs को डीबग करने के लिए, Chrome DevTools में मीडिया पैनल का इस्तेमाल करें.

WebCodecs को डीबग करने के लिए, मीडिया पैनल का स्क्रीनशॉट
WebCodecs की गड़बड़ी को डीबग करने के लिए, Chrome DevTools में मीडिया पैनल.

डेमो

नीचे दिए गए डेमो में, कैनवस से एनिमेशन फ़्रेम बनाने का तरीका बताया गया है:

  • MediaStreamTrackProcessor ने ReadableStream में 25fps पर कैप्चर किया
  • वेब वर्कर को ट्रांसफ़र किया गया
  • H.264 वीडियो फ़ॉर्मैट में एन्कोड किया गया हो
  • फिर से वीडियो फ़्रेम के क्रम में डिकोड किया जाता है
  • और transferControlToOffscreen() का इस्तेमाल करके दूसरे कैनवस पर रेंडर किया जाता है

अन्य डेमो

हमारे अन्य डेमो भी देखें:

WebCodecs API का इस्तेमाल करना

फ़ीचर का पता लगाना

WebCodecs के साथ काम करने की सुविधा की जांच करने के लिए:

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

ध्यान रखें कि WebCodecs API सिर्फ़ सुरक्षित कॉन्टेक्स्ट में उपलब्ध है. इसलिए, self.isSecureContext के गलत होने पर पहचान नहीं हो पाएगी.

सुझाव/राय दें या शिकायत करें

Chrome टीम, WebCodecs API के साथ आपके अनुभव जानना चाहती है.

हमें एपीआई के डिज़ाइन के बारे में बताएं

क्या एपीआई में कुछ ऐसा है जो आपकी उम्मीद के मुताबिक काम नहीं करता? क्या आपके आइडिया को लागू करने के लिए, कोई ऐसा तरीका या प्रॉपर्टी मौजूद नहीं है? क्या आपको सुरक्षा मॉडल के बारे में कोई सवाल पूछना है या कोई टिप्पणी करनी है? GitHub पर मौजूद डेटा स्टोर करने की जगह पर, स्पेसिफ़िकेशन से जुड़ी समस्या दर्ज करें या किसी मौजूदा समस्या में अपने सुझाव जोड़ें.

लागू करने से जुड़ी समस्या की शिकायत करना

क्या आपको Chrome में इस सुविधा को लागू करने में कोई गड़बड़ी मिली? या क्या इसे लागू करने का तरीका, स्पेसिफ़िकेशन से अलग है? new.crbug.com पर गड़बड़ी की शिकायत करें. ज़्यादा से ज़्यादा जानकारी दें और गड़बड़ी को दोहराने के लिए आसान निर्देश दें. साथ ही, Components बॉक्स में Blink>Media>WebCodecs डालें. Glitch, तुरंत और आसानी से समस्या की जानकारी शेयर करने के लिए बहुत अच्छा है.

एपीआई के लिए सहायता दिखाना

क्या आपको WebCodecs API का इस्तेमाल करना है? सार्वजनिक तौर पर सहायता करने से, Chrome टीम को सुविधाओं को प्राथमिकता देने में मदद मिलती है. साथ ही, इससे अन्य ब्राउज़र वेंडर को यह पता चलता है कि इन सुविधाओं को उपलब्ध कराना कितना ज़रूरी है.

media-dev@chromium.org पर ईमेल भेजें या #WebCodecs हैशटैग का इस्तेमाल करके, @ChromiumDev को ट्वीट करें. साथ ही, हमें बताएं कि इसका इस्तेमाल कहां और कैसे किया जा रहा है.

Unsplash पर, डेनिस जॉन्स की हीरो इमेज.