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 एपीआई उन वेब ऐप्लिकेशन के लिए काम का है जिन्हें मीडिया कॉन्टेंट को प्रोसेस करने के तरीके पर पूरा कंट्रोल चाहिए. जैसे, वीडियो एडिटर, वीडियो कॉन्फ़्रेंसिंग, वीडियो स्ट्रीमिंग वगैरह.

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

वीडियो प्रोसेसिंग के दौरान, फ़्रेम सबसे अहम होते हैं. इसलिए, 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);
    

चाहे वे कहीं से भी हों, फ़्रेम को 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() कॉलबैक को कॉल करके गड़बड़ी की रिपोर्ट की जाती है. अगर कोड में बदलने का तरीका सही तरीके से पूरा हो जाता है, तो 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();

डिकोड किया जा रहा है

नेटवर्क या स्टोरेज से कैनवस या ImageBitmap तक का पाथ.
नेटवर्क या स्टोरेज से Canvas या ImageBitmap तक का पाथ.

VideoDecoder को सेट अप करना, VideoEncoder के लिए किए गए फ़ंक्शन के जैसा ही है: डिकोडर बनाए जाने पर दो फ़ंक्शन पास किए जाते हैं और configure() को कोडेक पैरामीटर दिए जाते हैं.

कोडेक पैरामीटर का सेट, अलग-अलग कोडेक के लिए अलग-अलग होता है. उदाहरण के लिए, H.264 कोडेक को एवीसीसी के बाइनरी ब्लॉब की ज़रूरत पड़ सकती है, बशर्ते उसे ऐनेक्स बी फ़ॉर्मैट (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 FPS (फ़्रेम प्रति सेकंड) पर कैप्चर किया गया
  • वेब वर्कर को ट्रांसफ़र किया गया
  • 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 पर गड़बड़ी की शिकायत करें. ज़्यादा से ज़्यादा जानकारी शामिल करें. इसे फिर से बनाने के आसान निर्देश दें और कॉम्पोनेंट बॉक्स में Blink>Media>WebCodecs डालें. Glitch का इस्तेमाल करके, तुरंत और आसान तरीकों को शेयर किया जा सकता है.

एपीआई के साथ काम करता है

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

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

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