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

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

Joshua Grossberg
Joshua Grossberg

Online video tüketimi, pandeminin başlangıcından beri hızlı bir şekilde arttı. Kullanıcılar TikTok, Instagram ve YouTube gibi platformlarda sonsuz ve yüksek kaliteli video izlemeye daha fazla zaman harcıyor. Dünyanın dört bir yanındaki kreatifler ve küçük işletme sahipleri, video içeriği oluşturmak için hızlı ve kullanımı kolay araçlara ihtiyaç duyar.

Kapwing gibi şirketler, güçlü API'leri ve performans araçlarını kullanan en yenileri kullanarak tüm bu video içeriklerini doğrudan web'de oluşturmayı mümkün hale getiriyor.

Kapwing hakkında

Kapwing, özellikle oyun yayıncıları, müzisyenler, YouTube içerik üreticileri ve meme'ler gibi gündelik 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 üretmenin kolay bir yoluna ihtiyaç duyan işletme sahipleri için önemli bir kaynaktır.

Kullanıcılar Kapwing'i belirli bir görev için arama yaparak keşfederler. Örneğin, "video nasıl kırpılır?", "videoma müzik ekleme" veya "videoyu yeniden boyutlandırma". Kullanıcılar, aradıkları şeyi tek bir tıklamayla uygulama mağazasına gitmek ve bir uygulamayı indirmek gibi

Kullanıcılar bu ilk tıklamadan sonra daha fazla işlem yapabilir. Ücretsiz şablonları keşfedebilir, yeni stok video katmanları ekleyebilir, altyazı ekleyebilir, videoları metne dönüştürebilir ve arka plan müziği yükleyebilirler.

Kapwing, web'e gerçek zamanlı düzenleme ve ortak çalışma özelliklerini nasıl getiriyor?

Web benzersiz avantajlar sağlasa da bazı zorluklar da beraberinde getiriyor. Kapwing'in çok çeşitli cihazlar ve ağ koşullarında karmaşık, çok katmanlı projelerin sorunsuz ve hassas bir şekilde oynatılmasını sağlamak gerekiyor. Bu amaç doğrultusunda, performans ve özellik hedeflerimize ulaşmak için ç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ı gerektirir ve mümkün olduğunda ağdan kaçınır. Kullanıcıların bir içeriğe genellikle bir kez eriştiği akış hizmetlerinin aksine, müşterilerimiz öğelerini yüklemeden günler, hatta aylar sonra bile sık sık yeniden kullanırlar.

IndexedDB, kullanıcılarımıza kalıcı dosya sistemi benzeri bir depolama alanı sağlamamıza olanak tanıyor. Sonuçta uygulamadaki medya isteklerinin% 90'ından fazlası yerel olarak karşılanıyor. IndexedDB'nin sistemimize entegre edilmesi çok kolaydı.

Uygulama yüklendiğinde çalışan bazı ortak plaka 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 iletiriz ve bir upgrade işlevi tanımlarız. Bu, başlatma veya gerektiğinde şemamızı güncellemek için kullanılır. Kararsız sistemlere sahip kullanıcılar için sorunların engellenmesinde yararlı bulduğumuz blocked ve blocking gibi hata işleme geri çağırmalarını iletiyoruz.

Son olarak, birincil anahtar (keyPath) tanımımıza dikkat edin. Örneğimizde bu, mediaLibraryID adını verdiğimiz benzersiz bir kimliktir. Bir kullanıcı, yükleyicimiz veya bir üçüncü taraf uzantısı aracılığıyla sistemimize bir medya içeriği eklediğinde, bu medyayı medya kitaplığımıza aşağıdaki kodu kullanarak 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, dahili olarak tanımlanmış kendi işlevimizdir. IndexedDB API'nin eşzamansız olması nedeniyle bu işlem, okuma-değiştirme-yazma türündeki tüm işlemler için gereklidir.

Şimdi dosyalara nasıl eriştiğimize bakalım. Aşağıda getAsset fonksiyonumuzu görebilirsiniz:

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 olan idbCache. IndexedDB hızlı olsa da yerel belleğe erişmek daha hızlıdır. Önbelleğin boyutunu yönettiğiniz sürece bu yaklaşımı öneririz.

IndexedDB'ye eş zamanlı erişimi engellemek için kullanılan subscribers dizisi, aksi takdirde yükleme sırasında yaygın olarak kullanılır.

Web Audio API'sı

Ses görselleştirme, video düzenlemede son derece önemlidir. Nedenini anlamak için düzenleyicinin ekran görüntüsüne göz atın:

Kapwing&#39;in editörünün medya menüsü, aralarında LinkedIn gibi belirli platformlara özel bazı şablonlar, video, ses ve animasyonu ayıran zaman çizelgesi, dışa aktarma kalitesi seçenekleriyle tuval düzenleyici, videonun önizlemesi ve daha pek çok özellik de dahil olmak üzere çeşitli şablonlar ve özel öğeler içeren bir menü sunuyor.

Bu, YouTube tarzı bir videodur ve uygulamamızda yaygın olarak kullanılır. Kullanıcılar klip boyunca çok fazla hareket etmez. Bu nedenle, zaman çizelgesindeki görsel küçük resimler bölümler arasında gezinmek için çok kullanışlı değildir. Öte yandan, ses dalga formu zirveleri ve dipleri gösterir. Vadiler ise genellikle kayıttaki ölü zamana karşılık gelir. Zaman çizelgesini yakınlaştırırsanız duraklamalara ve duraklamalara karşılık gelen vadileri içeren daha ayrıntılı ses bilgileri görürsünüz.

Kullanıcı araştırmamız, içeriklerini birleştirirken içerik üreticilerin genellikle bu dalga formlarına göre yönlendirildiğini gösteriyor. Web Audio API'si bu bilgileri etkili bir şekilde sunmamıza ve zaman çizelgesini yakınlaştırarak veya kaydırarak hızlı bir şekilde güncellememizi sağlar.

Bunu nasıl yaptığımızı aşağıdaki snippet'te görebilirsiniz:

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. Tamamlandıktan sonra IndexedDB'deki öğeyi ve kendi önbelleğimizi güncelleyeceğiz.

AudioContext oluşturucusuyla audioBuffer hakkında veri toplarız, ancak cihaz donanımını oluşturmadığımızdan genlik verilerini depolayacağımız bir ArrayBuffer üzerinde oluşturmak için OfflineAudioContext kodunu kullanırız.

API'nin kendisi, etkili görselleştirme için gerekenden çok daha yüksek bir örnek hızında veri döndürür. Bu nedenle, değerleri manuel olarak 200 Hz'e düşürdük. Bunun faydalı ve görsel açıdan ilgi çekici dalga biçimleri için yeterli olduğunu gördük.

WebCodecs

Belirli videolarda parça küçük resimleri, zaman çizelgesinde gezinme açısından dalga formlarından daha yararlıdır. Ancak küçük resim üretmek, dalga biçimi oluşturmaktan daha fazla kaynak kullanır.

Yüklendiğinde tüm potansiyel küçük resimleri önbelleğe kaydedemeyiz. Bu nedenle, zaman çizelgesinde kaydırma/yakınlaştırma işlevinin hızlı çözülmesi, etkili ve duyarlı bir uygulama için kritik öneme sahiptir. Pürüzsüz kare çizimi yapmanın önündeki en büyük engel, karelerin kodunu çözmektir. Bu işlemi yakın zamana kadar bir HTML5 video oynatıcı kullanarak yapıyorduk. Bu yaklaşımın performansı güvenilir değildi ve çerçeve oluşturma sırasında uygulama duyarlılığında genellikle düşüş yaşadık.

Kısa bir süre önce, web çalışanlarında da kullanılabilen WebCodecs'a geçtik. Bu, ana iş parçacığı performansını etkilemeden çok sayıda katman için küçük resim çizme kabiliyetimizi geliştirecektir. Web çalışanı uygulaması devam ederken, mevcut ana iş parçacığı uygulamamızın özetini aşağıda bulabilirsiniz.

Bir video dosyası birden çok akış içerir: Video, ses ve altyazılar birlikte "muxleştirilir". WebCodecs'i kullanmak için önce ayrıştırılmış bir video akışımızın olması gerekir. mp4'leri, aşağıda gösterildiği gibi mp4box kitaplığıyla mumu çıkarırı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, MP4Box arayüzünü kapsamak için kullandığımız demuxer sınıfına işaret eder. Varlığa IndexedDB'den tekrar erişiyoruz. Bu segmentler bayt sırasına göre depolanmaz ve appendBuffer yöntemi bir sonraki parçanın göreli konumunu döndürür.

Bir video karesinin 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 };
};

Demuxer'ın yapısı oldukça karmaşıktır ve bu makalenin kapsamı dışındadır. Her kareyi samples başlıklı bir dizide depolar. Video kodunu çözmeyi başlatmamız gereken zaman damgası olan istediğimiz zaman damgasına giden en yakın ana kareyi bulmak için demuxer'ı kullanırız.

Videolar, anahtar veya i-çerçeve olarak bilinen tam karelerin yanı sıra genellikle p veya b-kareler olarak adlandırılan çok daha küçük delta karelerden oluşur. Kod çözme işlemi her zaman bir animasyon karesinden başlamalıdır.

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

  1. Kod çözücüyü, çerçeve çıkışı geri çağırmasıyla örneklendirme.
  2. Kod çözücüyü belirli codec ve giriş çözünürlüğü için yapılandırma.
  3. Demuxer'daki verileri kullanarak bir encodedVideoChunk oluşturma
  4. decodeEncodedFrame yöntemi çağrılıyor.

Bunu, istenen zaman damgasına sahip kareye ulaşana kadar yaparız.

Sonraki adım

Ölçeği ön ucumuzda, projeler büyüdükçe ve karmaşık hale geldikçe hassas ve yüksek performanslı oynatmayı sürdürebilmek olarak tanımlıyoruz. Performansı ölçeklendirmenin bir yolu, aynı anda mümkün olduğunca az sayıda video eklemektir. Ancak bunu yaptığımızda yavaş ve düzensiz geçiş 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 ne kadar kontrol sağlayabileceğiyle ilgili sınırlamalar vardır.

Gelecekte tüm medyaları WebCodecs kullanarak oynatmayı deneyebiliriz. Bu, performansın ölçeklendirilmesine yardımcı olması için hangi verileri arabelleğe aldığımız konusunda çok net olmamızı sağlayabilir.

Ayrıca, büyük izleme yüzeyi hesaplamalarını web çalışanlarına boşaltma konusunda daha iyi bir iş çıkarabilir, dosyaları önceden getirme ve çerçeveleri önceden oluşturma konusunda daha akıllı davranabiliriz. Genel uygulama performansımızı optimize etmek ve WebGL gibi araçlarla işlevleri 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 yaptığımız yatırıma devam etmek istiyoruz. Nesne algılama, özellik çıkarma, stil aktarımı gibi gelişmiş görevler için TensorFlow.js'den yararlanmayı planlıyoruz.

Sonuç olarak, ürünümüzü özgür ve açık bir web'de yerel benzeri performans ve işlevlerle geliştirmeye devam etmekten heyecan duyuyoruz.