Kapwing: Web için güçlü video düzenleme

İçerik üreticiler, güçlü API'ler (IndexedDB ve WebCodecs gibi) ve performans araçları sayesinde artık Kapwing ile yüksek kaliteli video içeriklerini web'de düzenleyebilirler.

Joshua Grossberg
Joshua Grossberg

Online video tüketimi, pandeminin başlangıcından bu yana hızla arttı. Kullanıcılar TikTok, Instagram ve YouTube gibi platformlarda sonsuz sayıda yüksek kaliteli videoya daha fazla zaman harcıyor. Dünyanın her yerinden reklamcılar 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, özellikle oyun yayıncıları, müzisyenler, YouTube içerik üreticileri ve meme'ler gibi basit reklam öğeleri için tasarlanmış web tabanlı, ortak çalışmaya dayalı bir video düzenleyicidir. 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 cihazlarda 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, tüm kullanıcılarımızın içeriğinin istemcide yayınlanmasını ve mümkün olduğunda ağdan uzak olması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'yi sistemimize entegre etmek çok kolay oldu.

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 iletir ve upgrade işlevi tanımlarız. 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. Bir kullanıcı, yükleyicimiz veya üçüncü taraf uzantısı aracılığıyla sistemimize medya içeriği eklediğinde, bu medyayı aşağıdaki kodu kullanarak 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 bakalı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şlemleri için son derece önemlidir. Bunun nedenini anlamak için düzenleyiciden bir ekran görüntüsüne göz atın:

Kapwing&#39;in düzenleyicisinde, aralarında LinkedIn gibi belirli platformlara özgü bazı şablonlar da dahil olmak üzere çeşitli şablonlar ve özel öğelerin de bulunduğu bir medya menüsü, video, ses ve animasyonu ayıran bir zaman çizelgesi, dışa aktarma kalitesi seçenekleriyle tuval düzenleyici, videonun önizlemesi ve daha fazla özellik bulunuyor.

Bu, uygulamamızda yaygın olarak tercih edilen YouTube tarzı bir video. Kullanıcı, klip boyunca çok fazla hareket etmiyor. Bu nedenle zaman çizelgesindeki görsel küçük resimler, bölümler arasında gezinmek için yeterince faydalı değildir. Diğer yandan, ses dalga formu zirveleri ve iniş çıkışları gösterir. Vadiler genellikle kayıttaki ölü zamana karşılık gelir. 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ırmalarımıza göre içerik üreticiler, içeriklerini birleştirirken genellikle bu dalga formlarının rehberliğinde hareket ediyor. 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ığında öğ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, faydalı 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.

Bir video dosyası birden çok akış içerir: video, ses, altyazılar vb. bu akışlar birlikte "mux" olur. WebCodecs'i kullanmak için önce video akışının çözülmesi 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. Bu öğ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ü yapısı oldukça karmaşıktır ve bu makalenin kapsamı dışındadır. Her kareyi samples adlı 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 her zaman bir animasyon karesinde başlamalıdır.

Uygulama, çerçevelerin kodunu şu şekilde çözer:

  1. Kod çözücüyü bir çerçeve çıkışı geri çağırma işleviyle örnekleme.
  2. Kod çözücüyü belirli codec'ler ve giriş çözünürlüğü için yapılandırma.
  3. Demuxer'daki verileri kullanarak encodedVideoChunk oluşturuluyor.
  4. 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 için önbelleğe almak üzere 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ğimizi 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.