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 में ज़्यादातर क्लास फ़्रेम का इस्तेमाल करती हैं या उन्हें बनाती हैं. वीडियो एन्कोडर, फ़्रेम को कोड में बदलते हुए छोटे हिस्सों में बदलते हैं. वीडियो डिकोडर, इसके उलट काम करते हैं.

साथ ही, 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() callback को कॉल करके भी गड़बड़ियों की शिकायत की जा सकती है. अगर कोड में बदलने का तरीका पूरा हो जाता है, तो 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 कोडेक को 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 पर, डेनिस जॉन्स की हीरो इमेज.