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 API, Insertable Streams API की क्लास के साथ मिलकर अच्छी तरह से काम करता है. ये क्लास, WebCodecs को मीडिया स्ट्रीम ट्रैक से कनेक्ट करती हैं.

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

WebCodecs और वेब वर्कर

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

इसके लिए, 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 });
    
  • 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);
    

फ़्रेम कहीं से भी आ रहे हों, उन्हें 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);
}

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

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

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

डेमो

डेमो में दिखाया गया है कि कैनवस से ऐनिमेशन फ़्रेम कैसे:

  • MediaStreamTrackProcessor ने ReadableStream में 25 फ़्रेम प्रति सेकंड की दर से रिकॉर्ड किया
  • वेब वर्कर को ट्रांसफ़र किया गया
  • 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 repo पर, स्पेसिफ़िकेशन से जुड़ी समस्या की शिकायत करें या किसी मौजूदा समस्या में अपने सुझाव जोड़ें.

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

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

एपीआई के लिए अपना सपोर्ट दिखाना

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

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

हीरो इमेज, डेनिस जैंस ने Unsplash पर शेयर की है.