Kapwing: ウェブ向けの高度な動画編集

Kapwing では、強力な API(IndexedDB や WebCodecs など)とパフォーマンス ツールを活用して、高品質な動画コンテンツをウェブ上で編集できるようになりました。

Joshua Grossberg
Joshua Grossberg

オンライン動画の視聴は、パンデミックが始まって以来、急速に増加しています。 人々は、高品質の動画を無限に楽しむために プラットフォーム(TikTok、Instagram、YouTube など)に接続する必要があります。クリエイティブと小規模ビジネス 世界中の所有者が、動画制作のための迅速かつ使いやすいツールを必要としています 説明します。

Kapwing のような企業は、こうした動画コンテンツをすべて正しく作成できるようにしています。 最新の強力な API とパフォーマンスツールを使用して

Kapwing について

Kapwing は、主にカジュアル ゲームストリーミングやミュージシャン、YouTube クリエイター、meme-r などのです。 また、簡単な方法を必要とするビジネス オーナーにとっても頼りになるリソースでもあります。 独自のソーシャル コンテンツ(Facebook や Instagram の広告など)を表示する。

ユーザーは、「動画の切り抜き方法」、「動画に音楽を追加する方法」、「動画のサイズを変更する方法」など、特定のタスクを検索して Kapwing を見つけます。アプリストアに移動してアプリをダウンロードする手間をかけることなく、検索した内容をワンクリックで実行できます。ウェブでは、サポートが必要なタスクを正確に検索して実行できます。

一度クリックすれば、Kapwing のユーザーはさらに多くのことができるようになります。ユーザーは、 無料テンプレートの確認、無料のストック動画の新しいレイヤの追加、 字幕、動画の文字起こし、バックグラウンド ミュージックのアップロードなどを行えます。

Kapwing がリアルタイム編集とコラボレーションをウェブで実現した方法

ウェブには独自のメリットがある一方で、 学びました。Kapwing は複雑で滑らかで正確な音声を マルチレイヤ プロジェクトを作成できます。 そのために、さまざまなウェブ API を使用して 見ていきましょう

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 関数を定義します。これは次の目的で使用されます。 必要に応じてスキーマを更新します。エラー処理は コールバック(blockedblocking)があります。これらは、 システムが不安定な場合にユーザーの問題を引き起こさないようにします。

最後に、主キー keyPath の定義に注目してください。この例では、mediaLibraryID という一意の ID です。ユーザーが YouTube のアップローダまたはサードパーティの拡張機能を介してシステムにメディアを追加すると、YouTube はメディアを追加します。 次のコードを追加します。

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;
}

Google には、IndexedDB を最小限に抑えるための独自のデータ構造 idbCache があります。 できます。IndexedDB は高速ですが、ローカルメモリへのアクセスは高速です。水 キャッシュのサイズを管理する場合は、この方法をおすすめします。

subscribers 配列。これは、次のリソースへの同時アクセスを防ぐために使用されます。 IndexedDB を使用します。それ以外の場合は、読み込み時に一般的です。

ウェブ オーディオ API

動画編集では、音声の可視化が非常に重要です。まず、 エディタのスクリーンショットをご覧ください。

Kapwing の編集ツールには、複数のテンプレートとカスタム要素を含むメディアのメニューがあります。LinkedIn などの特定のプラットフォームに固有のテンプレート、動画、音声、アニメーションを分離するタイムライン、エクスポート品質オプション付きのキャンバス エディタ、動画のプレビューなど、さまざまな機能があります。

これは YouTube スタイルの動画で、アプリでは一般的です。ユーザーがクリップ内をあまり移動しないため、タイムラインのビジュアル サムネイルはセクション間の移動にはあまり役立ちません。一方、音声は 波形: 山と谷を示します。 典型的には記録のデッドタイムに相当する部分です。タイムラインを拡大すると、途切れや一時停止に対応する谷がある、よりきめ細かいオーディオ情報が表示されます。

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 にレンダリングし、そこに格納する 出力します。

この API 自体は、必要なデータよりもはるかに高いサンプルレートでデータを返します。 説明します。そのため、Google では 200 Hz に手動でダウンサンプリングしています。これは、視覚的に魅力的で有用な波形に十分であることがわかったためです。

WebCodecs

特定の動画では、トラックのサムネイルがタイムラインに役立つ 理解しやすいでしょう。ただし サムネイルの生成は 複雑な場合もあるでしょう。

読み込み時にすべてのサムネイルをキャッシュに保存することはできないため、タイムラインのパン / ズームでの高速デコードは、パフォーマンスと応答性に優れたアプリにとって重要です。「 スムーズなフレーム描画を実現するボトルネックは、フレームのデコードです。これは、 最近は HTML5 動画プレーヤーを利用しましたこのアプローチのパフォーマンスは、 信頼性が低く、フレーム処理時のアプリの応答性の低下がしばしば 説明します。

最近、WebCodecs に移行しました。WebCodecs は ウェブワーカーですこれにより、大きなサイズのサムネイルを描画する機能が強化されるはずです。 レイヤ数を最適化します。ウェブが ワーカーの実装がまだ進行中のため、以下に概要を 実装する必要があります。

動画ファイルには、動画、音声、字幕などの複数のストリームが含まれます。 「多重化」されている説明します。WebCodec を使用するには、まず、分離された動画が必要です。 。以下に示すように、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 };
};

デマルチプレクサの構造は非常に複雑であり、この記事の範囲外です。各フレームを samples というタイトルの配列に格納します。デマルチプレクサーを使用して、 タイムスタンプに最も近いキーフレームを 動画のデコードを開始する必要があります。

動画は、キーまたは i フレームと呼ばれる完全なフレームと、 p フレームまたは b フレームと呼ばれる小さなデルタフレームです。デコードは常に キーフレームから開始できます。

アプリケーションは次の方法でフレームをデコードします。

  1. フレーム出力コールバックを使用してデコーダをインスタンス化する。
  2. 特定のコーデックと入力解像度に対応するようにデコーダを構成する。
  3. デマルチプレクサーのデータを使用して encodedVideoChunk を作成します。
  4. decodeEncodedFrame メソッドを呼び出す。

目的のタイムスタンプを含むフレームに到達するまで、この操作を繰り返します。

次のステップ

Google では、フロントエンドにおけるスケールとは、正確で信頼性の高い プロジェクトが大規模化して複雑化しても、パフォーマンスが向上します。パフォーマンスをスケーリングする 1 つの方法は、一度にマウントする動画をできるだけ少なくすることですが、そうすると、切り替えが遅く途切れる可能性があります。内部向けの開発が 再利用するために動画コンポーネントをキャッシュに保存する場合、 HTML5 動画タグが提供する HTML5 動画タグを管理できます

将来的には、WebCodecs を使用してすべてのメディアの再生を試みる可能性があります。この場合、 どのデータをバッファリングし スケーリングに役立つかを正確に 向上します

また、大規模なトラックパッドの計算をオフロードして、 web Worker など)、 フレームの事前生成などを行います。Google は自社のビジネス オペレーションを アプリケーションの全体的なパフォーマンスを向上させ、 WebGL

Google は今後も TensorFlow.js: 現在、 インテリジェントな背景削除。今後、TensorFlow.js は オブジェクト検出、特徴抽出、スタイル転送などの高度なタスクにも使用できます。

最終的には ネイティブアプリのようなプラットフォームで パフォーマンスと機能性が、自由でオープンなウェブで実現しています。