শক্তিশালী API (যেমন IndexedDB এবং WebCodecs) এবং পারফরম্যান্স টুলের জন্য ধন্যবাদ, নির্মাতারা এখন Kapwing-এর মাধ্যমে ওয়েবে উচ্চ-মানের ভিডিও সামগ্রী সম্পাদনা করতে পারেন।
মহামারী শুরু হওয়ার পর থেকে অনলাইন ভিডিও ব্যবহার দ্রুত বৃদ্ধি পেয়েছে। লোকেরা টিকটক, ইনস্টাগ্রাম এবং ইউটিউবের মতো প্ল্যাটফর্মগুলিতে অবিরাম উচ্চ-মানের ভিডিওতে আরও বেশি সময় ব্যয় করছে। সারা বিশ্বে ক্রিয়েটিভ এবং ছোট ব্যবসার মালিকদের ভিডিও সামগ্রী তৈরি করার জন্য দ্রুত এবং সহজেই ব্যবহারযোগ্য সরঞ্জামগুলির প্রয়োজন৷
কাপউইং-এর মতো কোম্পানিগুলি শক্তিশালী API এবং পারফরম্যান্স টুলগুলির সর্বশেষ ব্যবহার করে সরাসরি ওয়েবে এই সমস্ত ভিডিও সামগ্রী তৈরি করা সম্ভব করে৷
কাপউইং সম্পর্কে
কাপউইং হল একটি ওয়েব-ভিত্তিক সহযোগী ভিডিও সম্পাদক যা মূলত গেম-স্ট্রীমার, মিউজিশিয়ান, ইউটিউব ক্রিয়েটর এবং মেমে-আরএস-এর মতো নৈমিত্তিক সৃজনশীলদের জন্য ডিজাইন করা হয়েছে। এটি ব্যবসার মালিকদের জন্যও একটি গো-টু রিসোর্স যাদের তাদের নিজস্ব সামাজিক সামগ্রী যেমন Facebook এবং Instagram বিজ্ঞাপন তৈরি করার একটি সহজ উপায় প্রয়োজন৷
লোকেরা একটি নির্দিষ্ট কাজের জন্য অনুসন্ধান করে ক্যাপউইং আবিষ্কার করে, উদাহরণস্বরূপ "কীভাবে একটি ভিডিও ট্রিম করতে হয়," "আমার ভিডিওতে সঙ্গীত যোগ করুন" বা "একটি ভিডিওর আকার পরিবর্তন করুন।" একটি অ্যাপ স্টোরে নেভিগেট করা এবং একটি অ্যাপ ডাউনলোড করার অতিরিক্ত ঘর্ষণ ছাড়াই শুধুমাত্র একটি ক্লিকের মাধ্যমে তারা যা অনুসন্ধান করেছে তা করতে পারে। ওয়েবটি লোকেদের জন্য সঠিকভাবে অনুসন্ধান করা সহজ করে তোলে যে তাদের কোন কাজটিতে সহায়তা প্রয়োজন, এবং তারপরে এটি করুন৷
সেই প্রথম ক্লিকের পরে, কাপউইং ব্যবহারকারীরা আরও অনেক কিছু করতে পারে। তারা বিনামূল্যের টেমপ্লেট অন্বেষণ করতে পারে, বিনামূল্যের স্টক ভিডিওর নতুন স্তর যোগ করতে পারে, সাবটাইটেল সন্নিবেশ করতে পারে, ভিডিও প্রতিলিপি করতে পারে এবং পটভূমি সঙ্গীত আপলোড করতে পারে।
কিভাবে Kapwing ওয়েবে রিয়েল-টাইম সম্পাদনা এবং সহযোগিতা নিয়ে আসে
যদিও ওয়েব অনন্য সুবিধা প্রদান করে, এটি স্বতন্ত্র চ্যালেঞ্জও উপস্থাপন করে। ক্যাপউইংকে বিস্তৃত ডিভাইস এবং নেটওয়ার্ক অবস্থার মধ্যে জটিল, বহু-স্তরযুক্ত প্রকল্পগুলির মসৃণ এবং সুনির্দিষ্ট প্লেব্যাক সরবরাহ করতে হবে। এটি অর্জন করতে, আমরা আমাদের কর্মক্ষমতা এবং বৈশিষ্ট্য লক্ষ্য অর্জনের জন্য বিভিন্ন ওয়েব API ব্যবহার করি।
ইনডেক্সডডিবি
উচ্চ কার্যসম্পাদন সম্পাদনের জন্য প্রয়োজন যে আমাদের সমস্ত ব্যবহারকারীর সামগ্রী ক্লায়েন্টে লাইভ, যখনই সম্ভব নেটওয়ার্ক এড়ানো। একটি স্ট্রিমিং পরিষেবার বিপরীতে, যেখানে ব্যবহারকারীরা সাধারণত একবার সামগ্রীর একটি অংশ অ্যাক্সেস করে, আমাদের গ্রাহকরা আপলোডের পরে, কয়েক দিন এমনকি কয়েক মাস পরে তাদের সম্পদগুলি ঘন ঘন পুনঃব্যবহার করে।
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-তে একযোগে অ্যাক্সেস রোধ করতে ব্যবহৃত হয়, অন্যথায় লোডের সময় সাধারণ হবে।
ওয়েব অডিও API
ভিডিও সম্পাদনার জন্য অডিও ভিজ্যুয়ালাইজেশন অবিশ্বাস্যভাবে গুরুত্বপূর্ণ। কেন বোঝার জন্য, সম্পাদক থেকে একটি স্ক্রিনশট দেখুন:
এটি একটি 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
ব্যবহার করি যেখানে আমরা প্রশস্ততা ডেটা সংরক্ষণ করব।
কার্যকরী ভিজ্যুয়ালাইজেশনের জন্য প্রয়োজনীয়তার চেয়ে অনেক বেশি নমুনা হারে API নিজেই ডেটা ফেরত দেয়। এই কারণেই আমরা ম্যানুয়ালি 200 Hz-এ নমুনা নামিয়েছি, যা আমরা দরকারী, দৃশ্যত আকর্ষণীয় তরঙ্গরূপের জন্য যথেষ্ট বলে মনে করেছি।
ওয়েবকোডেক্স
নির্দিষ্ট কিছু ভিডিওর জন্য ট্র্যাক থাম্বনেইল ওয়েভফর্মের চেয়ে টাইমলাইন নেভিগেশনের জন্য বেশি উপযোগী। যাইহোক, থাম্বনেইল তৈরি করা ওয়েভফর্ম তৈরি করার চেয়ে বেশি সম্পদ নিবিড়।
আমরা লোডের সময় প্রতিটি সম্ভাব্য থাম্বনেইল ক্যাশে করতে পারি না, তাই টাইমলাইন প্যান/জুমে দ্রুত ডিকোড করা একটি পারফরম্যান্ট এবং প্রতিক্রিয়াশীল অ্যাপ্লিকেশনের জন্য গুরুত্বপূর্ণ। মসৃণ ফ্রেম অঙ্কন অর্জনের বাধা হল ফ্রেমের ডিকোডিং, যা সম্প্রতি পর্যন্ত আমরা একটি HTML5 ভিডিও প্লেয়ার ব্যবহার করেছিলাম। সেই পদ্ধতির পারফরম্যান্স নির্ভরযোগ্য ছিল না এবং আমরা প্রায়শই ফ্রেম রেন্ডারিংয়ের সময় অবনমিত অ্যাপ প্রতিক্রিয়াশীলতা দেখেছি।
সম্প্রতি আমরা WebCodecs- এ চলে এসেছি, যা ওয়েব কর্মীদের মধ্যে ব্যবহার করা যেতে পারে। এটি প্রধান থ্রেড কর্মক্ষমতা প্রভাবিত না করে বৃহৎ পরিমাণ স্তরের জন্য থাম্বনেইল আঁকার আমাদের ক্ষমতা বাড়াতে হবে। যখন ওয়েব কর্মী বাস্তবায়ন এখনও চলছে, আমরা আমাদের বিদ্যমান মূল থ্রেড বাস্তবায়নের নীচে একটি রূপরেখা দিচ্ছি।
একটি ভিডিও ফাইলে একাধিক স্ট্রীম রয়েছে: ভিডিও, অডিও, সাবটাইটেল এবং আরও অনেক কিছু একসাথে 'মিক্সড'। WebCodecs ব্যবহার করার জন্য, আমাদের প্রথমে একটি demuxed ভিডিও স্ট্রিম থাকতে হবে। আমরা mp4box লাইব্রেরির সাথে mp4s demux করি, যেমনটি এখানে দেখানো হয়েছে:
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 };
};
demuxer এর গঠন বেশ জটিল এবং এই নিবন্ধের সুযোগের বাইরে। এটি প্রতিটি ফ্রেমকে একটি অ্যারে শিরোনামের samples
সংরক্ষণ করে। আমরা আমাদের পছন্দসই টাইমস্ট্যাম্পের নিকটতম পূর্ববর্তী কী ফ্রেমটি খুঁজে পেতে demuxer ব্যবহার করি, যেখানে আমাদের অবশ্যই ভিডিও ডিকোড শুরু করতে হবে।
ভিডিওগুলি পূর্ণ ফ্রেমগুলির সমন্বয়ে গঠিত, যা কী বা আই-ফ্রেম হিসাবে পরিচিত, সেইসাথে অনেক ছোট ডেল্টা ফ্রেমগুলিকে প্রায়ই পি- বা বি-ফ্রেম হিসাবে উল্লেখ করা হয়। ডিকোড সর্বদা একটি কী ফ্রেমে শুরু হওয়া আবশ্যক।
অ্যাপ্লিকেশনটি ফ্রেমগুলিকে ডিকোড করে:
- একটি ফ্রেম আউটপুট কলব্যাক সহ ডিকোডারকে ইনস্ট্যান্টিয়েটিং।
- নির্দিষ্ট কোডেক এবং ইনপুট রেজোলিউশনের জন্য ডিকোডার কনফিগার করা হচ্ছে।
- ডেমুক্সার থেকে ডেটা ব্যবহার করে একটি
encodedVideoChunk
তৈরি করা হচ্ছে। -
decodeEncodedFrame
পদ্ধতিতে কল করা হচ্ছে।
আমরা কাঙ্খিত টাইমস্ট্যাম্প সহ ফ্রেমে পৌঁছানো পর্যন্ত এটি করি।
এরপর কি?
আমরা আমাদের ফ্রন্টএন্ডে স্কেলকে সুনির্দিষ্ট এবং পারফরম্যান্স প্লেব্যাক বজায় রাখার ক্ষমতা হিসাবে সংজ্ঞায়িত করি কারণ প্রকল্পগুলি বড় এবং আরও জটিল হয়৷ পারফরম্যান্স স্কেল করার একটি উপায় হল একবারে যতটা সম্ভব কম ভিডিও মাউন্ট করা, তবে আমরা যখন এটি করি, তখন আমরা ধীরগতির এবং বিচ্ছিন্ন পরিবর্তনের ঝুঁকি নিয়ে থাকি। যদিও আমরা পুনঃব্যবহারের জন্য ভিডিও উপাদান ক্যাশে করার জন্য অভ্যন্তরীণ সিস্টেমগুলি তৈরি করেছি, HTML5 ভিডিও ট্যাগগুলি কতটা নিয়ন্ত্রণ করতে পারে তার সীমাবদ্ধতা রয়েছে৷
ভবিষ্যতে, আমরা WebCodecs ব্যবহার করে সমস্ত মিডিয়া চালানোর চেষ্টা করতে পারি। এটি আমাদেরকে আমরা কোন ডেটা বাফার করি যা পারফরম্যান্সকে স্কেল করতে সহায়তা করবে সে সম্পর্কে খুব সুনির্দিষ্ট হতে দেয়।
এছাড়াও আমরা ওয়েব কর্মীদের কাছে বৃহৎ ট্র্যাকপ্যাড কম্পিউটেশন অফলোড করার আরও ভালো কাজ করতে পারি, এবং আমরা প্রি-ফেচিং ফাইল এবং ফ্রেম প্রি-জেনারেট করার বিষয়ে আরও স্মার্ট হতে পারি। আমরা আমাদের সামগ্রিক অ্যাপ্লিকেশন কার্যকারিতা অপ্টিমাইজ করার এবং WebGL এর মতো সরঞ্জামগুলির সাথে কার্যকারিতা প্রসারিত করার বড় সুযোগ দেখতে পাচ্ছি।
আমরা TensorFlow.js- এ আমাদের বিনিয়োগ চালিয়ে যেতে চাই, যা আমরা বর্তমানে বুদ্ধিমান ব্যাকগ্রাউন্ড অপসারণের জন্য ব্যবহার করি। আমরা অন্যান্য পরিশীলিত কাজ যেমন অবজেক্ট ডিটেকশন, ফিচার এক্সট্রাকশন, স্টাইল ট্রান্সফার ইত্যাদির জন্য TensorFlow.js ব্যবহার করার পরিকল্পনা করছি।
শেষ পর্যন্ত, আমরা একটি বিনামূল্যে এবং উন্মুক্ত ওয়েবে নেটিভ-সদৃশ কর্মক্ষমতা এবং কার্যকারিতা সহ আমাদের পণ্য নির্মাণ চালিয়ে যেতে উত্তেজিত।