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 का इस्तेमाल

एन्कोडिंग

कैनवस या 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 की वैल्यू 'गलत' है, तो पहचान नहीं हो पाएगी.

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

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

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

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

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

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

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

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

[email protected] पर ईमेल भेजें या @ChromiumDev को ट्वीट करें. इसके लिए, हैशटैग #WebCodecs का इस्तेमाल करें. साथ ही, हमें बताएं कि इसका इस्तेमाल कहां और कैसे किया जा रहा है.

हीरो इमेज Denise Jans ने Unsplash पर शेयर की है.