İçerik üreticiler artık güçlü API'ler (IndexedDB ve WebCodecs gibi) ve performans araçları sayesinde Kapwing ile web'de yüksek kaliteli video içerikleri düzenleyebilir.
Online video tüketimi, pandeminin başlangıcından bu yana hızla arttı. Kullanıcılar TikTok, Instagram ve YouTube gibi platformlarda yüksek kaliteli videoları daha uzun süre izliyor. Dünyanın dört bir yanındaki içerik üreticiler ve küçük işletme sahipleri, video içeriği oluşturmak için hızlı ve kullanımı kolay araçlara ihtiyaç duyuyor.
Kapwing gibi şirketler, güçlü API'ler ve performans araçlarındaki en son gelişmeleri kullanarak tüm bu video içeriklerini doğrudan web'de oluşturmayı mümkün kılıyor.
Kapwing hakkında
Kapwing, web tabanlı bir ortak çalışma video düzenleyicisidir. Oyun yayıncıları, müzisyenler, YouTube içerik üreticileri ve meme'ler gibi içerik üreticiler için tasarlanmıştır. Ayrıca, Facebook ve Instagram reklamları gibi kendi sosyal içeriklerini kolayca üretmek isteyen işletme sahipleri için de bir başvuru kaynağıdır.
Kullanıcılar, "videoyu kırpma", "videoma müzik ekleme" veya "videoyu yeniden boyutlandırma" gibi belirli bir görevi ararken Kapwing'i keşfeder. Kullanıcılar, arama yaptıkları işlemi tek tıklamayla yapabilir. Bu işlem için uygulama mağazasına gidip uygulama indirmeleri gerekmez. Web, kullanıcıların yardıma ihtiyaç duydukları görevi tam olarak aramalarını ve ardından bu görevi yapmalarını kolaylaştırır.
İlk tıklamadan sonra Kapwing kullanıcıları çok daha fazlasını yapabilir. Ücretsiz şablonları keşfedebilir, ücretsiz stok videolardan yeni katmanlar ekleyebilir, altyazı ekleyebilir, videoları metne dönüştürebilir ve arka plan müziği yükleyebilirler.
Kapwing, gerçek zamanlı düzenleme ve ortak çalışmayı web'e nasıl getiriyor?
Web benzersiz avantajlar sunarken farklı zorluklar da getirir. Kapwing'in, karmaşık ve çok katmanlı projeleri çeşitli cihaz ve ağ koşullarında sorunsuz ve hassas bir şekilde oynatması gerekir. Bu amaca ulaşmak için performans ve özellik hedeflerimize ulaşmak amacıyla çeşitli web API'leri kullanırız.
IndexedDB
Yüksek performanslı düzenleme, kullanıcılarımızın tüm içeriklerinin mümkün olduğunda ağdan kaçınarak istemcide yayınlanmasını gerektirir. Kullanıcıların genellikle bir içeriğe bir kez eriştiği bir akış hizmetinin aksine, müşterilerimiz öğelerini yükleme işleminden günler ve hatta aylar sonra sık sık yeniden kullanır.
IndexedDB, kullanıcılarımıza kalıcı dosya sistemi benzeri depolama alanı sunmamızı sağlar. Sonuç olarak, uygulamadaki medya isteklerinin% 90'ından fazlası yerel olarak karşılanır. IndexedDB'i sistemimize entegre etmek çok kolaydı.
Uygulama yüklendiğinde çalışan bazı standart başlatma kodları aşağıda verilmiştir:
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');
},
}
);
Bir sürüm iletiyoruz ve upgrade
işlevi tanıyoruz. Bu, ilk başlatma için veya gerektiğinde şemamızı güncellemek için kullanılır. Hata işleme geri çağırma işlevlerini (blocked
ve blocking
) iletiriz. Bu işlevler, kararsız sistemleri olan kullanıcıların sorunlarını önlemede faydalı olduğunu tespit ettiğimiz işlevlerdir.
Son olarak, birincil anahtar keyPath
tanımımıza dikkat edin. Bu örnekte, mediaLibraryID
olarak adlandırdığımız benzersiz bir kimlik kullanılır. Kullanıcılar yükleme aracımız veya üçüncü taraf uzantısı aracılığıyla sistemimize medya eklediğinde, medyayı aşağıdaki kodla medya kitaplığımıza ekleriz:
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 erişimini serileştiren kendi dahili olarak tanımlanmış işlevimizdir. IndexedDB API'si eşzamansız olduğundan, tüm okuma-değiştirme-yazma türü işlemler için bu gereklidir.
Şimdi dosyalara nasıl eriştiğimize göz atalım. getAsset
işlevimiz aşağıda verilmiştir:
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;
}
IndexedDB erişimlerini en aza indirmek için kullanılan kendi veri yapımız (idbCache
) vardır. IndexedDB hızlı olsa da yerel belleğe erişim daha hızlıdır. Önbelleğin boyutunu yönettiğiniz sürece bu yaklaşımı öneririz.
IndexedDB'e eşzamanlı erişimi önlemek için kullanılan subscribers
dizisi, aksi takdirde yükleme sırasında yaygın olur.
Web Audio API
Ses görselleştirme, video düzenleme için son derece önemlidir. Bunun nedenini anlamak için düzenleyiciden bir ekran görüntüsüne göz atın:
Bu, uygulamamızda yaygın olarak kullanılan YouTube tarzı bir videodur. Kullanıcı klip boyunca çok fazla hareket etmediğinden zaman çizelgelerinin görsel küçük resimleri, bölümler arasında gezinmek için çok yararlı değildir. Öte yandan ses dalga biçimi, zirve ve vadileri gösterir. Vadilerin genellikle kayıttaki sessiz alanlara karşılık geldiği unutulmamalıdır. Zaman çizelgesini yakınlaştırırsanız takılmalara ve duraklamalara karşılık gelen çukurlar içeren daha ayrıntılı ses bilgileri görürsünüz.
Kullanıcı araştırmamız, içerik üreticilerin içeriklerini birleştirirken genellikle bu dalga formlarından yararlandığını gösteriyor. Web ses API'si, bu bilgileri etkili bir şekilde sunmamıza ve zaman çizelgesinin yakınlaştırılması veya kaydırılmasına göre hızlı bir şekilde güncelleme yapmamıza olanak tanır.
Aşağıdaki snippet'te bunu nasıl yaptığımız gösterilmektedir:
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;
}
);
Bu yardımcıya IndexedDB'de depolanan öğeyi iletiriz. İşlem tamamlandıktan sonra öğeyi IndexedDB'de ve kendi önbelleğimizde güncelleriz.
audioBuffer
ile ilgili verileri AudioContext
kurucusunu kullanarak toplarız ancak cihaz donanımına oluşturma işlemi uygulamadığımız için genlik verilerini depolayacağımız bir ArrayBuffer
oluşturmak için OfflineAudioContext
'yi kullanırız.
API'nin kendisi, etkili görselleştirme için gerekenden çok daha yüksek bir örnekleme hızında veri döndürür. Bu nedenle, kullanışlı ve görsel açıdan ilgi çekici dalga formları için yeterli olduğunu tespit ettiğimiz 200 Hz'ye manuel olarak örnekleme sıklığını düşürürüz.
WebCodecs
Belirli videolarda parça küçük resimleri, zaman çizelgesi gezinme için dalga biçimlerinden daha kullanışlıdır. Ancak küçük resim oluşturmak, dalga formları oluşturmaktan daha fazla kaynak gerektirir.
Yükleme sırasında her olası küçük resmi önbelleğe alamayız. Bu nedenle, zaman çizelgesinde hızlı kod çözme/yakınlaştırma/uzaklaştırma, performanslı ve duyarlı bir uygulama için kritik öneme sahiptir. Sorunsuz kare çizimi elde etmenin önündeki en büyük engel, karelerin kodunun çözülmesidir. Yakın zamana kadar bu işlemi HTML5 video oynatıcı kullanarak yapıyorduk. Bu yaklaşımın performansı güvenilir değildi ve kare oluşturma sırasında uygulamanın yanıt vermesi sık sık bozuluyordu.
Yakın zamanda, web işçilerinde kullanılabilen WebCodecs'e geçtik. Bu sayede, ana iş parçacığı performansını etkilemeden çok sayıda katman için küçük resim çizme özelliğimizi geliştirebiliriz. Web işleyici uygulaması hâlâ devam ederken mevcut ana iş parçacığı uygulamamızın ana hatlarını aşağıda bulabilirsiniz.
Video dosyaları birden fazla akış içerir: video, ses, altyazı vb. bu akışlar birlikte "birleştirilir". WebCodecs'i kullanmak için önce video akışının ses ve görüntüden ayrılmış olması gerekir. MP4'leri, burada gösterildiği gibi mp4box kitaplığıyla demux yaparız:
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);
}
};
Bu snippet, arayüzü MP4Box
içine almak için kullandığımız bir demuxer
sınıfını ifade eder. Öğeye IndexedDB'den tekrar erişiriz. Bu segmentler mutlaka bayt sırasına göre depolanmaz ve appendBuffer
yöntemi bir sonraki parçanın ofsetini döndürür.
Video çerçevesinin kodunu şu şekilde çözeriz:
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 };
};
Çözücünün yapısı oldukça karmaşıktır ve bu makalenin kapsamı dışındadır. Her kareyi samples
başlıklı bir dizede depolar. İstediğimiz zaman damgasına en yakın önceki anahtar kareyi bulmak için videoyu çözmeye başlamamız gereken noktadaki videoyu çözme işlemini başlatmak için videoyu çözme aracını kullanırız.
Videolar, anahtar veya i-kareler olarak bilinen tam karelerin yanı sıra genellikle p- veya b-kareler olarak adlandırılan çok daha küçük delta karelerinden oluşur. Kod çözme işlemi her zaman bir animasyon karesinden başlamalıdır.
Uygulama, karelerin kodunu şu şekilde çözer:
- Kod çözücüyü bir çerçeve çıkışı geri çağırma işleviyle örnekleme.
- Kod çözücüyü belirli codec'ler ve giriş çözünürlüğü için yapılandırma.
- Çözücüden alınan verileri kullanarak bir
encodedVideoChunk
oluşturma. decodeEncodedFrame
yöntemini çağırma.
İstediğimiz zaman damgasını içeren kareye ulaşana kadar bu işlemi tekrarlarız.
Sırada ne var?
Kullanıcı arayüzünde ölçeği, projeler büyüdükçe ve daha karmaşık hale geldikçe hassas ve performanslı oynatmayı sürdürme olarak tanımlarız. Performansı ölçeklendirmenin bir yolu, tek seferde mümkün olduğunca az videoyu monte etmektir. Ancak bunu yaptığımızda yavaş ve kesintili geçişler riskiyle karşı karşıya kalırız. Video bileşenlerini yeniden kullanmak üzere önbelleğe almak için dahili sistemler geliştirmiş olsak da HTML5 video etiketlerinin sağlayabileceği kontrol düzeyi sınırlıdır.
Gelecekte tüm medyaları WebCodecs kullanarak oynatmayı deneyebiliriz. Bu sayede, hangi verileri arabelleğe alacağımız konusunda çok hassas olabiliriz. Bu da performansı ölçeklendirmeye yardımcı olur.
Ayrıca büyük izleme yüzeyi hesaplamalarını web işleyicilerine aktarma konusunda daha iyi bir iş çıkarabilir ve dosyaları önceden getirme ve kareleri önceden oluşturma konusunda daha akıllıca davranabiliriz. Genel uygulama performansımızı optimize etmek ve WebGL gibi araçlarla işlevselliği genişletmek için büyük fırsatlar görüyoruz.
Şu anda akıllı arka plan kaldırma için kullandığımız TensorFlow.js'ye yatırım yapmaya devam etmek istiyoruz. Nesne algılama, özellik ayıklama, stil aktarımı gibi karmaşık görevler için TensorFlow.js'den yararlanmayı planlıyoruz.
Özetle, ürünümüze özgür ve açık bir web'de yerele benzer performans ve işlevler eklemeye devam etmekten heyecan duyuyoruz.