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

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

Joshua Grossberg
Joshua Grossberg

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

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

कापविंग के बारे में जानकारी

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

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

उस पहले क्लिक के बाद, 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 हमारा खुद का अंदरूनी तौर पर तय किया गया फ़ंक्शन है, जो इंडेक्स किया गया DB ऐक्सेस को क्रम से लगाता है. यह किसी भी पढ़ने-बदलने-लिखने के टाइप से जुड़ी कार्रवाइयों के लिए ज़रूरी है, क्योंकि 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 कलेक्शन, जिसका इस्तेमाल इंडेक्सेडडीबी को एक साथ ऐक्सेस करने से रोकने के लिए किया जाता है, लोड होने पर आम तौर पर यही होता है.

वेब ऑडियो एपीआई

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

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

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

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

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

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 के बारे में डेटा इकट्ठा करते हैं. हालांकि, हम डिवाइस हार्डवेयर को रेंडर नहीं करने की वजह से, ArrayBuffer पर रेंडर करने के लिए OfflineAudioContext का इस्तेमाल करते हैं, जहां हम डाइमेंशन वाला डेटा सेव करेंगे.

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

WebCodecs

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

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

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

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

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 टाइटल वाली कैटगरी में सेव करता है. हम डिमक्सर का इस्तेमाल अपने मनचाहे टाइमस्टैंप के सबसे करीब वाले मुख्य फ़्रेम का पता लगाने के लिए करते हैं. यहीं से हमें वीडियो डिकोड करना शुरू करना होगा.

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

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

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

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

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

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

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

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

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

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