क्रिएटर्स अब Kapwing की मदद से, वेब पर अच्छी क्वालिटी के वीडियो कॉन्टेंट में बदलाव कर सकते हैं. इसके लिए, IndexedDB और WebCodecs जैसे बेहतर एपीआई और परफ़ॉर्मेंस टूल का इस्तेमाल किया जाता है.
महामारी की शुरुआत से, ऑनलाइन वीडियो देखने का चलन तेज़ी से बढ़ा है. लोग 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
वीडियो एडिट करने के लिए, ऑडियो विज़ुअलाइज़ेशन का होना बहुत ज़रूरी है. इसकी वजह जानने के लिए, एडिटर का स्क्रीनशॉट देखें:
यह 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-फ़्रेम कहा जाता है. डिकोड करने की प्रोसेस हमेशा किसी मुख्य फ़्रेम से शुरू होनी चाहिए.
ऐप्लिकेशन, इन तरीकों से फ़्रेम को डिकोड करता है:
- फ़्रेम आउटपुट कॉलबैक की मदद से, डिकोडर को इंस्टैंशिएट करना.
- किसी खास कोडेक और इनपुट रिज़ॉल्यूशन के लिए, डिकोडर को कॉन्फ़िगर करना.
- डिम्यूक्सर के डेटा का इस्तेमाल करके
encodedVideoChunk
बनाना. decodeEncodedFrame
वाले तरीके को कॉल करना.
हम ऐसा तब तक करते हैं, जब तक कि हमें टाइमस्टैंप वाला फ़्रेम नहीं मिल जाता.
आगे क्या करना है?
हम अपने फ़्रंटएंड पर स्केल को इस तरह से परिभाषित करते हैं कि प्रोजेक्ट बड़े और ज़्यादा जटिल होने पर भी, वीडियो को सटीक और बेहतर तरीके से चलाया जा सके. परफ़ॉर्मेंस को बेहतर बनाने का एक तरीका यह है कि एक साथ कम से कम वीडियो माउंट किए जाएं. हालांकि, ऐसा करने पर ट्रांज़िशन धीमे और रुक-रुककर हो सकते हैं. हमने वीडियो कॉम्पोनेंट को फिर से इस्तेमाल करने के लिए, कैश मेमोरी में सेव करने के लिए इंटरनल सिस्टम डेवलप किए हैं. हालांकि, HTML5 वीडियो टैग से कंट्रोल करने की सीमाएं हैं.
आने वाले समय में, हम WebCodecs का इस्तेमाल करके सभी मीडिया चलाने की कोशिश कर सकते हैं. इससे, हमें यह तय करने में मदद मिलती है कि कौनसा डेटा बफ़र करना है. इससे परफ़ॉर्मेंस को बेहतर बनाने में मदद मिलती है.
हम ट्रैकपैड के बड़े कैलकुलेशन को वेब वर्कर्स पर ऑफ़लोड करने का बेहतर काम कर सकते हैं. साथ ही, हम फ़ाइलों को पहले से फ़ेच करने और फ़्रेम को पहले से जनरेट करने के बारे में बेहतर तरीके से सोच सकते हैं. हमें अपने ऐप्लिकेशन की परफ़ॉर्मेंस को ऑप्टिमाइज़ करने और WebGL जैसे टूल की मदद से, सुविधाओं को बेहतर बनाने के कई अवसर दिख रहे हैं.
हम TensorFlow.js में लगातार काम करना चाहते हैं. फ़िलहाल, हम इसका इस्तेमाल बैकग्राउंड को बेहतर तरीके से हटाने के लिए करते हैं. हम TensorFlow.js का इस्तेमाल, ऑब्जेक्ट का पता लगाने, फ़ीचर निकालने, स्टाइल ट्रांसफ़र करने वगैरह जैसे दूसरे मुश्किल कामों के लिए करने जा रहे हैं.
आखिर में, हमें खुशी है कि हम अपने प्रॉडक्ट को बिना किसी शुल्क के उपलब्ध कराए जा रहे ओपन वेब पर, नेटिव प्रॉडक्ट जैसी परफ़ॉर्मेंस और फ़ंक्शन के साथ बना रहे हैं.