कापविंग: वेब पर वीडियो एडिटिंग की बेहतर सुविधा

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

Joshua Grossberg
Joshua Grossberg

महामारी शुरू होने के बाद से, ऑनलाइन वीडियो देखने की संख्या तेज़ी से बढ़ी है. लोग TikTok, Instagram, और YouTube जैसे प्लैटफ़ॉर्म पर, अच्छी क्वालिटी के अनलिमिटेड वीडियो देखने में ज़्यादा समय बिता रहे हैं. दुनिया भर के क्रिएटर्स और छोटे कारोबार के मालिकों को वीडियो कॉन्टेंट बनाने के लिए, तुरंत और आसानी से इस्तेमाल किए जा सकने वाले टूल की ज़रूरत होती है.

Kapwing जैसी कंपनियां, वेब पर ही यह सारा वीडियो कॉन्टेंट बनाने की सुविधा देती हैं. इसके लिए, वे नए और बेहतर एपीआई और परफ़ॉर्मेंस टूल का इस्तेमाल करती हैं.

Kapwing के बारे में जानकारी

Kapwing, वेब पर आधारित एक वीडियो एडिटर है. इसे मुख्य रूप से गेम स्ट्रीम करने वाले लोगों, संगीतकारों, YouTube क्रिएटर्स, और मीम बनाने वाले लोगों के लिए बनाया गया है. यह उन कारोबार के मालिकों के लिए भी एक अच्छा संसाधन है जिन्हें Facebook और Instagram विज्ञापनों जैसे सोशल कॉन्टेंट को आसानी से बनाने की ज़रूरत है.

Kapwing को लोग किसी खास टास्क को खोजकर खोजते हैं. जैसे, "वीडियो में काट-छांट करने का तरीका," "मेरे वीडियो में संगीत जोड़ें" या "वीडियो का साइज़ बदलें." वे सिर्फ़ एक क्लिक से, अपनी खोज के नतीजे पा सकते हैं. इसके लिए, उन्हें ऐप्लिकेशन स्टोर पर जाने और ऐप्लिकेशन डाउनलोड करने की ज़रूरत नहीं होती. वेब की मदद से, लोग आसानी से यह खोज सकते हैं कि उन्हें किस काम में मदद चाहिए और फिर उसे पूरा कर सकते हैं.

पहले क्लिक के बाद, Kapwing के उपयोगकर्ता बहुत कुछ कर सकते हैं. वे बिना किसी शुल्क के उपलब्ध टेंप्लेट एक्सप्लोर कर सकते हैं. साथ ही, बिना किसी शुल्क के उपलब्ध स्टॉक वीडियो की नई लेयर जोड़ सकते हैं, सबटाइटल डाल सकते हैं, वीडियो का लेखा ट्रांसक्राइब कर सकते हैं, और बैकग्राउंड में संगीत अपलोड कर सकते हैं.

Kapwing, रीयल-टाइम में वेब पर एडिटिंग और साथ मिलकर काम करने की सुविधा कैसे देता है

वेब के कई फ़ायदे हैं, लेकिन इसमें कुछ चुनौतियां भी हैं. Kapwing को कई तरह के डिवाइसों और नेटवर्क की स्थितियों पर, मुश्किल और कई लेयर वाले प्रोजेक्ट को आसानी से और सटीक तरीके से चलाना होता है. ऐसा करने के लिए, हम अपनी परफ़ॉर्मेंस और सुविधाओं के लक्ष्यों को हासिल करने के लिए, कई तरह के वेब एपीआई का इस्तेमाल करते हैं.

IndexedDB

बेहतर परफ़ॉर्मेंस के साथ वीडियो एडिट करने के लिए, ज़रूरी है कि हमारे सभी उपयोगकर्ताओं का कॉन्टेंट क्लाइंट पर मौजूद हो. साथ ही, जब भी हो सके, नेटवर्क का इस्तेमाल न किया जाए. स्ट्रीमिंग सेवा के उलट, जहां उपयोगकर्ता आम तौर पर किसी कॉन्टेंट को एक बार ऐक्सेस करते हैं, वहीं हमारे ग्राहक अपलोड करने के कुछ दिनों या महीनों बाद भी अपनी एसेट का बार-बार इस्तेमाल करते हैं.

IndexedDB की मदद से हम अपने उपयोगकर्ताओं को स्थायी फ़ाइल सिस्टम जैसा स्टोरेज उपलब्ध करा सकते हैं. इस वजह से, ऐप्लिकेशन में 90% से ज़्यादा मीडिया अनुरोध, स्थानीय तौर पर पूरे किए जाते हैं. IndexedDB को अपने सिस्टम में जोड़ना बहुत आसान था.

यहां कुछ बोइलर प्लेट कोड दिया गया है, जो ऐप्लिकेशन लोड होने पर चलता है:

import {DBSchema, openDB, deleteDB, IDBPDatabase} from 'idb';

let openIdb: Promise <IDBPDatabase<Schema>>;

const db =
  (await openDB) <
  Schema >
  (
    'kapwing',
    version, {
      upgrade(db, oldVersion) {
        if (oldVersion >= 1) {
          // assets store schema changed, need to recreate
          db.deleteObjectStore('assets');
        }

        db.createObjectStore('assets', {
          keyPath: 'mediaLibraryID'
        });
      },
      async blocked() {
        await deleteDB('kapwing');
      },
      async blocking() {
        await deleteDB('kapwing');
      },
    }
  );

हम एक वर्शन पास करते हैं और upgrade फ़ंक्शन तय करते हैं. इसका इस्तेमाल, शुरू करने या ज़रूरत पड़ने पर हमारे स्कीमा को अपडेट करने के लिए किया जाता है. हम गड़बड़ियों को ठीक करने के लिए कॉलबैक, blocked, और blocking पास करते हैं. इनकी मदद से, हम उन उपयोगकर्ताओं को समस्याओं को रोकने में मदद करते हैं जिनके सिस्टम में बदलाव नहीं होता है.

आखिर में, प्राइमरी की keyPath की हमारी परिभाषा पर ध्यान दें. हमारे मामले में, यह एक ऐसा यूनीक आईडी है जिसे हम mediaLibraryID कहते हैं. जब कोई उपयोगकर्ता हमारे सिस्टम में कोई मीडिया जोड़ता है, तो हम उस मीडिया को अपनी मीडिया लाइब्रेरी में जोड़ देते हैं. भले ही, वह मीडिया हमारे अपलोड करने वाले टूल या तीसरे पक्ष के एक्सटेंशन से जोड़ा गया हो. इसके लिए, हम इस कोड का इस्तेमाल करते हैं:

export async function addAsset(mediaLibraryID: string, file: File) {
  return runWithAssetMutex(mediaLibraryID, async () => {
    const assetAlreadyInStore = await (await openIdb).get(
      'assets',
      mediaLibraryID
    );    
    if (assetAlreadyInStore) return;
        
    const idbVideo: IdbVideo = {
      file,
      mediaLibraryID,
    };

    await (await openIdb).add('assets', idbVideo);
  });
}

runWithAssetMutex, हमारे अंदर से तय किया गया फ़ंक्शन है, जो IndexedDB ऐक्सेस को सीरियलाइज़ करता है. यह किसी भी तरह के पढ़ने-बदलाव करने-लिखने वाले ऑपरेशन के लिए ज़रूरी है, क्योंकि IndexedDB API असींक्रोनस है.

चलिए, अब देखते हैं कि हम फ़ाइलों को कैसे ऐक्सेस करते हैं. यहां हमारा getAsset फ़ंक्शन दिया गया है:

export async function getAsset(
  mediaLibraryID: string,
  source: LayerSource | null | undefined,
  location: string
): Promise<IdbAsset | undefined> {
  let asset: IdbAsset | undefined;
  const { idbCache } = window;
  const assetInCache = idbCache[mediaLibraryID];

  if (assetInCache && assetInCache.status === 'complete') {
    asset = assetInCache.asset;
  } else if (assetInCache && assetInCache.status === 'pending') {
    asset = await new Promise((res) => {
      assetInCache.subscribers.push(res);
    }); 
  } else {
    idbCache[mediaLibraryID] = { subscribers: [], status: 'pending' };
    asset = (await openIdb).get('assets', mediaLibraryID);

    idbCache[mediaLibraryID].asset = asset;
    idbCache[mediaLibraryID].subscribers.forEach((res: any) => {
      res(asset);
    });

    delete (idbCache[mediaLibraryID] as any).subscribers;

    if (asset) {
      idbCache[mediaLibraryID].status = 'complete';
    } else {
      idbCache[mediaLibraryID].status = 'failed';
    }
  } 
  return asset;
}

हमारे पास अपना डेटा स्ट्रक्चर, idbCache है. इसका इस्तेमाल, IndexedDB के ऐक्सेस को कम करने के लिए किया जाता है. IndexedDB तेज़ है, लेकिन लोकल मेमोरी को ऐक्सेस करना ज़्यादा तेज़ है. हम इस तरीके का सुझाव तब तक देते हैं, जब तक कैश मेमोरी का साइज़ मैनेज किया जा रहा हो.

subscribers कलेक्शन का इस्तेमाल, IndexedDB को एक साथ ऐक्सेस करने से रोकने के लिए किया जाता है. हालांकि, यह लोड होने पर आम तौर पर ऐसा होता है.

Web Audio API

वीडियो एडिट करने के लिए, ऑडियो विज़ुअलाइज़ेशन का होना बहुत ज़रूरी है. इसकी वजह जानने के लिए, एडिटर का स्क्रीनशॉट देखें:

Kapwing के एडिटर में मीडिया के लिए एक मेन्यू है. इसमें कई टेंप्लेट और कस्टम एलिमेंट शामिल हैं. इनमें LinkedIn जैसे प्लैटफ़ॉर्म के लिए खास तौर पर बनाए गए कुछ टेंप्लेट, वीडियो, ऑडियो, और ऐनिमेशन को अलग करने वाली टाइमलाइन, एक्सपोर्ट क्वालिटी के विकल्पों के साथ कैनवस एडिटर, वीडियो की झलक, और अन्य सुविधाएं शामिल हैं.

यह YouTube स्टाइल का वीडियो है, जो हमारे ऐप्लिकेशन में आम तौर पर दिखता है. इस क्लिप में, उपयोगकर्ता का कैमरा एक ही जगह पर रहता है. इसलिए, सेक्शन के बीच नेविगेट करने के लिए, टाइमलाइन के विज़ुअल थंबनेल उतने काम के नहीं हैं. वहीं दूसरी ओर, ऑडियो वेवफ़ॉर्म में चोटियों और घाटियों को दिखाया जाता है. आम तौर पर, रिकॉर्डिंग में समय की कमी को ध्यान में रखकर दिखाया जाता है. टाइमलाइन पर ज़ूम इन करने पर, आपको ऑडियो की ज़्यादा जानकारी दिखेगी. इसमें, रुक-रुककर चलने और रुकने के हिसाब से वैली दिखेंगी.

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

नीचे दिया गया स्निपेट दिखाता है कि हम यह कैसे करते हैं:

const getDownsampledBuffer = (idbAsset: IdbAsset) =>
  decodeMutex.runExclusive(
    async (): Promise<Float32Array> => {
      const arrayBuffer = await idbAsset.file.arrayBuffer();
      const audioContext = new AudioContext();
      const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);

      const offline = new OfflineAudioContext(
        audioBuffer.numberOfChannels,
        audioBuffer.duration * MIN_BROWSER_SUPPORTED_SAMPLE_RATE,
        MIN_BROWSER_SUPPORTED_SAMPLE_RATE
      );

      const downsampleSource = offline.createBufferSource();
      downsampleSource.buffer = audioBuffer;
      downsampleSource.start(0);
      downsampleSource.connect(offline.destination);

      const downsampledBuffer22K = await offline.startRendering();

      const downsampledBuffer22KData = downsampledBuffer22K.getChannelData(0);

      const downsampledBuffer = new Float32Array(
        Math.floor(
          downsampledBuffer22KData.length / POST_BROWSER_SAMPLE_INTERVAL
        )
      );

      for (
        let i = 0, j = 0;
        i < downsampledBuffer22KData.length;
        i += POST_BROWSER_SAMPLE_INTERVAL, j += 1
      ) {
        let sum = 0;
        for (let k = 0; k < POST_BROWSER_SAMPLE_INTERVAL; k += 1) {
          sum += Math.abs(downsampledBuffer22KData[i + k]);
        }
        const avg = sum / POST_BROWSER_SAMPLE_INTERVAL;
        downsampledBuffer[j] = avg;
      }

      return downsampledBuffer;
    } 
  );

हम इस हेल्पर को IndexedDB में सेव की गई ऐसेट पास करते हैं. प्रोसेस पूरी होने के बाद, हम IndexedDB और अपनी कैश मेमोरी में ऐसेट को अपडेट कर देंगे.

हम AudioContext कन्स्ट्रक्टर की मदद से audioBuffer के बारे में डेटा इकट्ठा करते हैं. हालांकि, हम डिवाइस के हार्डवेयर पर रेंडर नहीं कर रहे हैं. इसलिए, हम OfflineAudioContext का इस्तेमाल करके ArrayBuffer पर रेंडर करते हैं, जहां हम ऐम्प्लitude डेटा सेव करेंगे.

एपीआई, बेहतर तरीके से विज़ुअलाइज़ेशन के लिए ज़रूरत से काफ़ी ज़्यादा सैंपल रेट पर डेटा दिखाता है. इसलिए, हम मैन्युअल रूप से 200 हर्ट्ज़ तक डाउनसैंपल करते हैं. हमें लगता है कि यह फ़्रीक्वेंसी, काम की और आकर्षक वेवफ़ॉर्म के लिए काफ़ी है.

WebCodecs

कुछ वीडियो के लिए, ट्रैक के थंबनेल, वॉवलफ़ॉर्म के मुकाबले टाइमलाइन पर नेविगेट करने के लिए ज़्यादा मददगार होते हैं. हालांकि, वेवफ़ॉर्म बनाने के मुकाबले थंबनेल बनाने में ज़्यादा संसाधन लगता है.

हम लोड होने पर, हर संभावित थंबनेल को कैश मेमोरी में सेव नहीं कर सकते. इसलिए, बेहतर परफ़ॉर्म करने वाले और रिस्पॉन्सिव ऐप्लिकेशन के लिए, टाइमलाइन पैन/ज़ूम को तुरंत डिकोड करना ज़रूरी होता है. फ़्रेम को आसानी से ड्रॉ करने में सबसे बड़ी समस्या, फ़्रेम को डिकोड करना है. हाल ही तक, हमने इसके लिए HTML5 वीडियो प्लेयर का इस्तेमाल किया था. इस तरीके की परफ़ॉर्मेंस भरोसेमंद नहीं थी. साथ ही, फ़्रेम रेंडर करने के दौरान, हमें अक्सर ऐप्लिकेशन के जवाब देने में देरी होती थी.

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

किसी वीडियो फ़ाइल में एक से ज़्यादा स्ट्रीम होती हैं: वीडियो, ऑडियो, सबटाइटल वगैरह. WebCodecs का इस्तेमाल करने के लिए, हमारे पास पहले से ही डिम्यूक्स की गई वीडियो स्ट्रीम होनी चाहिए. हम mp4box लाइब्रेरी की मदद से, mp4 फ़ाइलों को अलग-अलग हिस्सों में बांटते हैं. इस बारे में यहां बताया गया है:

async function create(demuxer: any) {
  demuxer.file = (await MP4Box).createFile();
  demuxer.file.onReady = (info: any) => {
    demuxer.info = info;
    demuxer._info_resolver(info);
  };
  demuxer.loadMetadata();
}

const loadMetadata = async () => {
  let offset = 0;
  const asset = await getAsset(this.mediaLibraryId, null, this.url);
  const maxFetchOffset = asset?.file.size || 0;

  const end = offset + FETCH_SIZE;
  const response = await fetch(this.url, {
    headers: { range: `bytes=${offset}-${end}` },
  });
  const reader = response.body.getReader();

  let done, value;
  while (!done) {
    ({ done, value } = await reader.read());
    if (done) {
      this.file.flush();
      break;
    }

    const buf: ArrayBufferLike & { fileStart?: number } = value.buffer;
    buf.fileStart = offset;
    offset = this.file.appendBuffer(buf);
  }
};

यह स्निपेट, demuxer क्लास का रेफ़रंस देता है. इसका इस्तेमाल, इंटरफ़ेस को MP4Box में शामिल करने के लिए किया जाता है. हम IndexedDB से एसेट को फिर से ऐक्सेस करते हैं. ज़रूरी नहीं है कि ये सेगमेंट, बाइट के क्रम में सेव किए गए हों. साथ ही, appendBuffer तरीका अगले चंक का ऑफ़सेट दिखाता है.

हम वीडियो फ़्रेम को इस तरह डिकोड करते हैं:

const getFrameFromVideoDecoder = async (demuxer: any): Promise<any> => {
  let desiredSampleIndex = demuxer.getFrameIndexForTimestamp(this.frameTime);
  let timestampToMatch: number;
  let decodedSample: VideoFrame | null = null;

  const outputCallback = (frame: VideoFrame) => {
    if (frame.timestamp === timestampToMatch) decodedSample = frame;
    else frame.close();
  };  

  const decoder = new VideoDecoder({
    output: outputCallback,
  }); 
  const {
    codec,
    codecWidth,
    codecHeight,
    description,
  } = demuxer.getDecoderConfigurationInfo();
  decoder.configure({ codec, codecWidth, codecHeight, description }); 

  /* begin demuxer interface */
  const preceedingKeyFrameIndex = demuxer.getPreceedingKeyFrameIndex(
    desiredSampleIndex
  );  
  const trak_id = demuxer.trak_id
  const trak = demuxer.moov.traks.find((trak: any) => trak.tkhd.track_id === trak_id);
  const data = await demuxer.getFrameDataRange(
    preceedingKeyFrameIndex,
    desiredSampleIndex
  );  
  /* end demuxer interface */

  for (let i = preceedingKeyFrameIndex; i <= desiredSampleIndex; i += 1) {
    const sample = trak.samples[i];
    const sampleData = data.readNBytes(
      sample.offset,
      sample.size
    );  

    const sampleType = sample.is_sync ? 'key' : 'delta';
    const encodedFrame = new EncodedVideoChunk({
      sampleType,
      timestamp: sample.cts,
      duration: sample.duration,
      samapleData,
    }); 

    if (i === desiredSampleIndex)
      timestampToMatch = encodedFrame.timestamp;
    decoder.decodeEncodedFrame(encodedFrame, i); 
  }
  await decoder.flush();

  return { type: 'value', value: decodedSample };
};

डीमक्सर की बनावट काफ़ी जटिल है और इस लेख के दायरे से बाहर है. यह हर फ़्रेम को samples टाइटल वाले कलेक्शन में सेव करता है. हम अपने पसंदीदा टाइमस्टैंप से पहले का सबसे नज़दीकी मुख्य फ़्रेम ढूंढने के लिए, डिम्यूज़र का इस्तेमाल करते हैं. यहीं से हमें वीडियो को डिकोड करना शुरू करना होता है.

वीडियो में फ़ुल फ़्रेम होते हैं, जिन्हें की या i-फ़्रेम कहा जाता है. साथ ही, बहुत छोटे डेल्टा फ़्रेम भी होते हैं, जिन्हें अक्सर p- या b-फ़्रेम कहा जाता है. डिकोड करने की प्रोसेस हमेशा किसी मुख्य फ़्रेम से शुरू होनी चाहिए.

ऐप्लिकेशन, इन तरीकों से फ़्रेम को डिकोड करता है:

  1. फ़्रेम आउटपुट कॉलबैक की मदद से, डिकोडर को इंस्टैंशिएट करना.
  2. खास कोडेक और इनपुट रिज़ॉल्यूशन के लिए डिकोडर कॉन्फ़िगर करना.
  3. डिम्यूक्सर के डेटा का इस्तेमाल करके encodedVideoChunk बनाना.
  4. decodeEncodedFrame वाले तरीके को कॉल करना.

हम ऐसा तब तक करते हैं, जब तक कि हमें टाइमस्टैंप वाला फ़्रेम नहीं मिल जाता.

आगे क्या करना है?

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

आने वाले समय में, हम WebCodecs का इस्तेमाल करके सभी मीडिया चलाने की कोशिश कर सकते हैं. इससे, हमें यह तय करने में मदद मिलती है कि कौनसा डेटा बफ़र करना है. इससे परफ़ॉर्मेंस को बेहतर बनाने में मदद मिलती है.

हम ट्रैकपैड के बड़े कैलकुलेशन को वेब वर्कर्स पर ऑफ़लोड करने का बेहतर काम कर सकते हैं. साथ ही, हम फ़ाइलों को पहले से फ़ेच करने और फ़्रेम को पहले से जनरेट करने के बारे में बेहतर तरीके से सोच सकते हैं. हमें अपने ऐप्लिकेशन की परफ़ॉर्मेंस को ऑप्टिमाइज़ करने और WebGL जैसे टूल की मदद से, सुविधाओं को बेहतर बनाने के कई अवसर दिख रहे हैं.

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

आखिर में, हमें खुशी है कि हम अपने प्रॉडक्ट को बिना किसी शुल्क के उपलब्ध कराए जा रहे ओपन वेब पर, नेटिव प्रॉडक्ट जैसी परफ़ॉर्मेंस और फ़ंक्शन के साथ बना रहे हैं.