エキサイティングな SaaS 構築の旅に着手してから 4 年が経過し、アプリの重要なコンポーネントの 1 つを再構築する適切な時期が来ました。
JavaScript で書かれたソーシャル メディア ビデオ用のシンプルなビデオ エディター。
これは、この書き換えに使用することに決めたスタックであり、現在作業中です。
私たちのフロントエンドは SvelteKit で書かれているため、これが私たちのユースケースに最適なオプションです。
ビデオ エディターは、フロントエンドに簡単に追加できる別個のプライベート npm ライブラリです。ヘッドレス ライブラリであるため、ビデオ エディターの UI は完全に分離されています。
ビデオ エディター ライブラリは、ビデオおよびオーディオ要素とタイムラインの同期、アニメーションとトランジションのレンダリング、HTML テキストのキャンバスへのレンダリングなどを行います。
SceneBuilderFactory は、シーン JSON オブジェクトを引数として受け取り、シーンを作成します。 StateManager.svelte.ts は、ビデオ エディターの現在の状態をリアルタイムで保持します。
これは、タイムラインでの再生ヘッドの位置の描画や更新などに非常に便利です。
Pixi.js は、優れた JavaScript キャンバス ライブラリです。
当初、私はこのプロジェクトを Pixi v8 でビルドし始めましたが、この記事の後半で説明するいくつかの理由により、Pixi v7 を使用することにしました。
ただし、ビデオ エディター ライブラリは依存関係と緊密に結合されていないため、必要に応じて依存関係を置き換えたり、別のツールをテストしたりするのは簡単です。
タイムライン管理と複雑なアニメーションには、GSAP を使用することにしました。
これほど簡単な方法で、ネストされたタイムライン、組み合わせたアニメーション、または複雑なテキスト アニメーションを構築できるツールは、JavaScript エコシステムで他にありません。
私は GSAP ビジネス ライセンスを持っているので、追加のツールを活用してより多くのことをシンプルにすることもできます。
バックエンドで使用するものについて詳しく説明する前に、JavaScript でビデオ エディタを構築する際に解決する必要があるいくつかの課題を見てみましょう。
この質問は GSAP フォーラムでよく聞かれます。
タイムライン管理に GSAP を使用するかどうかは関係ありません。必要なことはいくつかあります。
各レンダリング ティック:
タイムラインに対するビデオの相対時間を取得します。ビデオがタイムラインの 10 秒マークで最初から再生を開始するとします。
そうですね、10 秒前まではビデオ要素は実際には気にしませんが、タイムラインに入ったらすぐに同期を保つ必要があります。
ビデオの相対時間を計算することでこれを行うことができます。これは、ビデオ要素の currentTime から計算し、現在のシーン時間と比較し、許容可能な「ラグ」期間内で計算する必要があります。
遅延が、たとえば 0.3 秒より大きい場合は、ビデオ要素を自動シークして、メイン タイムラインとの同期を修正する必要があります。これはオーディオ要素にも当てはまります。
考慮する必要があるその他の事項:
再生と一時停止は簡単に実装できます。シークのために、ビデオシークコンポーネント ID を svelte StateManager に追加します。これにより、状態が自動的に「読み込み中」に変更されます。
StateManager には EventManager 依存関係があり、状態が変化するたびに自動的に「changestate」イベントがトリガーされるため、$effect を使用せずにこれらのイベントをリッスンできます。
シークが終了し、ビデオを再生する準備ができた後も、同じことが起こります。
これにより、一部のコンポーネントの読み込み時に UI に再生/一時停止ボタンの代わりに読み込みインジケーターを表示できます。
CSS、GSAP、および GSAP の TextSplitter を使用すると、テキスト要素で本当に素晴らしいことができます。
ネイティブ キャンバス テキスト要素は制限されており、アプリの主な使用例はソーシャル メディア用の短い形式のビデオを作成することであるため、適切ではありません。
幸いなことに、ほぼすべての HTML テキストをキャンバスにレンダリングする方法を見つけました。これはビデオ出力のレンダリングに不可欠です。
Pixi HTMLText
これが最も単純な解決策だったでしょう。残念ながら、私には効果がありませんでした。
GSAP を使用して HTML テキストをアニメーション化すると、大幅に遅れが生じ、また、GSAP で試した多くの Google フォントもサポートされませんでした。
サトリ
Satori は素晴らしいもので、より単純なユースケースで使用されることが想像できます。残念ながら、一部の GSAP アニメーションは、Satori と互換性のないスタイルを変更するため、エラーが発生します。
異物を含む SVG
最後に、これを解決するためのカスタム ソリューションを作成しました。
難しい部分は絵文字とカスタム フォントのサポートでしたが、なんとか解決できました。
次のような SVG を生成する、generateSVG メソッドを持つ SVGGenerator クラスを作成しました。
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
styleTag は次のようになります:
<style>@font-face { font-family: ${fontFamilyName}; src: url('${fontData}') }</style>
これが機能するには、渡す HTML のインライン スタイル内に正しいフォント ファミリが設定されている必要があります。フォント データは、data:font/ttf;base64,longboringstring
のような、base64 でエンコードされたデータ文字列である必要があります。継承よりも構成、と彼らは言います。
実際に手を動かす練習として、継承ベースのアプローチからフックベースのシステムにリファクタリングしました。
私のビデオエディタでは、VIDEO、AUDIO、TEXT、SUBTITLES、IMAGE、SHAPE などの要素をコンポーネントと呼んでいます。
これを書き換える前は、BaseComponent という抽象クラスがあり、各コンポーネント クラスはそれを拡張しており、VideoComponent にはビデオなどのロジックがありました。
問題は、すぐにめちゃくちゃになってしまうことでした。
コンポーネントは、レンダリング方法、Pixi テクスチャの管理方法、アニメーション方法などを担当します。
現在、コンポーネント クラスは 1 つだけあり、非常に単純です。
これには 4 つのライフサイクル イベントが含まれています:
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
このコンポーネント クラスには、動作を変更する addHook というメソッドがあります。
フックはコンポーネントのライフサイクル イベントにフックし、アクションを実行できます。
たとえば、ビデオとオーディオのコンポーネントに使用する MediaHook があります。
MediaHook は、基礎となるオーディオ要素またはビデオ要素を作成し、それをメインのタイムラインと自動的に同期させます。
コンポーネントを構築するために、ディレクター パターンとともにビルダー パターンを使用しました (リファレンスを参照)。
このようにして、オーディオ コンポーネントを構築するときに、それに MediaHook を追加し、ビデオ コンポーネントにも追加します。ただし、ビデオには次の追加のフックも必要です。
このアプローチにより、レンダリング ロジックやシーン内でのコンポーネントの動作の変更、拡張、修正が非常に簡単になります。
最速かつ最もコスト効率の高い方法でビデオをレンダリングする方法について、複数の異なるアプローチを試しました。
2020 年、私は最も単純なアプローチ、つまり多くのツールで行われているフレームを次々にレンダリングすることから始めました。
試行錯誤した後、レンダリング レイヤーのアプローチに切り替えました。
これは、SceneData ドキュメントにコンポーネントを含むレイヤーが含まれていることを意味します。
これらの各レイヤーは個別にレンダリングされ、ffmpeg と結合されて最終出力が作成されます。
レイヤーには同じタイプのコンポーネントのみを含めることができるという制限がありました。
たとえば、ビデオを含むレイヤーにはテキスト要素を含めることはできません。他のビデオのみを含めることができます。
これには明らかに長所と短所があります。
Lambda 上でアニメーション付きの HTML テキストを個別にレンダリングし、透明なビデオに変換し、最終出力のために他のチャンクと組み合わせるのは非常に簡単でした。
一方、ビデオコンポーネントを含むレイヤーは ffmpeg で単純に処理されました。
しかし、このアプローチには大きな欠点がありました。
ビデオの拡大縮小、フェード、回転を行うキーフレーム システムを実装したい場合は、fluent-ffmpeg でこれらの機能のポートを作成する必要があります。
それは間違いなく可能ですが、私には他のすべての責任があるため、それができませんでした。
そこで、最初のアプローチ、つまり 1 つのフレームを次々にレンダリングする方法に戻ることにしました。
レンダリング リクエストは Express を使用してバックエンド サーバーに送信されます。
このルートは、ビデオがまだレンダリングされていないかどうかを確認し、レンダリングされていない場合は、BullMQ キューに追加します。
キューがレンダリングの処理を開始すると、ヘッドレス Chrome の複数のインスタンスが生成されます。
注: この処理は、AMD EPYC 7502P 32 コア プロセッサーと 128 GB RAM を搭載した専用の Hetzner サーバーで実行されるため、非常にパフォーマンスの高いマシンです。
Chromium にはコーデックがないので、Chrome のインストールが簡単になる Playwright を使用していることに注意してください。
それでも、何らかの理由でビデオ フレームが真っ黒になってしまいました。
きっと何かが足りなかったんだと思います。ただし、ビデオを使用する代わりに、ビデオ コンポーネントを個々の画像フレームに分割し、サーバーレス ブラウザで使用することにしました。
それでも、最も重要なのはスクリーンショットの使用を避けることです。
すべてが 1 つのキャンバスにあるため、キャンバス上で .getDataURL() を使用して画像に取り込むことができ、はるかに高速になります。
これを簡単にするために、ビデオエディタをバンドルし、いくつかの機能をウィンドウに追加する静的ページを作成しました。
これは Playwright/Puppeteer でロードされ、各フレームで次のように呼び出すだけです。
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" version="1.1">${styleTag}<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="transform-origin: 0 0;">${html}</div></foreignObject></svg>
これにより、画像として保存するか、ビデオ チャンクをレンダリングするためにバッファに追加できるフレーム データが得られます。
このプロセス全体は、ビデオの長さに応じて 5 ~ 10 個の異なるワーカーに分割され、最終出力にマージされます。
これの代わりに、Lambda などにオフロードすることもできますが、私は RunPod を使用することに傾いています。彼らのサーバーレス アーキテクチャの唯一の欠点は、私があまり詳しくない Python を使用していることです。
この方法では、レンダリングがクラウド上で処理される複数のチャンクに分割される可能性があり、60 分のビデオのレンダリングでも 1 ~ 2 分で完了できます。それは嬉しいことですが、それは私たちの主な目標や使用例ではありません。
Pixi 8 から Pixi 7 にダウングレードした理由は、Pixi 7 には 2D キャンバスをサポートする「レガシー」バージョンもあるためです。これはレンダリングがはるかに高速です。 60 秒のビデオはサーバー上でレンダリングするのに約 80 秒かかりますが、キャンバスに WebGL または WebGPU コンテキストがある場合、1 秒あたり 1 ~ 2 フレームしかレンダリングできませんでした。
興味深いことに、私のテストによると、WebGL キャンバスをレンダリングする際、サーバーレス Chrome はヘッドフル Firefox に比べてはるかに遅かったです。
専用の GPU を使用しても、レンダリングの速度は大幅に向上しませんでした。私が何か間違ったことをしたか、単にヘッドレス Chrome が WebGL であまりパフォーマンスが良くないかのどちらかです。
私たちのユースケースにおける WebGL は、通常非常に短い遷移に最適です。
これに関してテストする予定の方法の 1 つは、WebGL チャンクと非 WebGL チャンクを別々にレンダリングすることです。
プロジェクトには多くの部分が関係しています。
ドキュメントの構造はスキーマレス データベースに保存することが最も合理的であるため、シーン データは MongoDB に保存されます。
SvelteKit で書かれたフロントエンドは、urql を GraphQL クライアントとして使用します。
GraphQL サーバーは、PHP Laravel と MongoDB および素晴らしい Lighthouse GraphQL を使用します。
しかし、これはおそらく次回のテーマです。
今回はここまでです!これを本番環境に導入し、現在のビデオ エディタを置き換える前に、やらなければならない作業がたくさんあります。このビデオ エディタは非常にバグが多く、フランケンシュタインを少し思い出させます。
あなたの意見を聞かせて、これからもロックを続けてください!
以上が単独開発者として TypeScript ビデオ エディタを構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。