同様にヘッドレスブラウザを用いてプログラマブルに動画を作成できるremotionの動画レンダリング技術が気になり、調べてみました。
準備
requestAnimationFrame
requestAnimationFrameは、Web標準のAPIです。引数にコールバックを渡すことで、「ブラウザが再描画可能なタイミング」でコールバックが実行されます。
requestAnimationFrame
を使わずに再描画処理をloop処理で行うと、描画可能でないタイミングで再描画処理が行われてしまう可能性があります。即ち画面上で表示されない再描画処理のiterationが生じてしまう可能性があります。
また、同じフレーム内に複数のrequestAnimationFrame
のコールバックがenqueueされた場合、それらのコールバックは同じフレームで実行されます。以下はmozillaのdocsからの引用です。timestampはひとつ前の描画フレームの終了時刻を指します。
When multiple callbacks queued by requestAnimationFrame() begin to fire in a single frame, each receives the same timestamp even though time has passed during the computation of every previous callback’s workload.
remotionのレンダリング方法
以下では、remotion v4.0.81の実装を基にremotionの動画レンダリング方法を読解します。
シーケンス図
以下がremotionによるレンダリング時のシーケンス図です。1
特徴的な点として、以下が挙げられます
- ヘッドレスブラウザがフレームを描画できる状態かどうかは
requestAnimationFrame
を用いて判断する。 - スクリーンショットを撮るAPIはJavaScriptには存在しない2ため、CDP(Chrome DevTools Protocol)のPage.captureScreenshotを用いる
- ブラウザが描画できる状態で
Page.captureScreenshot
を実行するため、以下要領で実行タイミングを決定する- Reactを実行しているヘッドレスブラウザ上でglobal変数(
remotion_renderReady
)に描画できる状態かどうかのフラグを持たせる - waitForReady関数から
remotion_renderReady
をpollingする。trueになったタイミングでscreenshotを撮る。React側でrequestAnimationFrame
によるcontinueRender
コールバックを実行できるようにするため、pollingのループはrequestAnimationFrame
で行う
- Reactを実行しているヘッドレスブラウザ上でglobal変数(
- ブラウザが描画できる状態で
sequenceDiagram participant renderer participant puppeteer participant browser participant remotion_renderReady participant react loop screenshot a frame Note over renderer,react: set the next frame renderer->>puppeteer: set the next frame puppeteer->>browser: remotion_setFrame
(Runtime.callFunctionOn) browser->>remotion_renderReady: delayRender remotion_renderReady->>remotion_renderReady: set false remotion_renderReady->>browser: done browser->>react: set frame context browser->>remotion_renderReady: continueRender
with requestAnimationFrame browser->>puppeteer: done puppeteer->>renderer: done par rerendering the next frame react->>react: update frame context and
rerender user-defined components remotion_renderReady->>remotion_renderReady: set true
with renderable timing end Note over renderer,react: wait ready state renderer->>puppeteer: wait ready puppeteer->>browser: onRaF
(Runtime.callFunctionOn) loop until remotion_renderReady being true browser->>remotion_renderReady: check value
with requestAnimationFrame remotion_renderReady->>browser: end browser->>puppeteer: done puppeteer->>renderer: ready Note over renderer,react: take screenshot renderer->>puppeteer: take screenshot puppeteer->>browser: Page.captureScreenshot(CDP) browser->>puppeteer: screenshot end
まとめ
remotionは、requestAnimationFrameとReactのContextを用いることにより、frameごとに正確な動画レンダリングを可能にするとても実用的なフレームワークです。
この記事では扱いませんでしたが、videoやaudioを埋め込む場合も、レンダリングした動画上で正確にそれらが再生されるような機構を設けています。
v2以前ではPuppetterがdependenciesに入っていましたが、v3以降では互換性を保ったまま内製化しています。この記事では内製化したものを"puppeteer"と表記しています ↩︎
getDisplayMediaというAPIは存在しますが、これはStreamを返すものです。これを使う場合最終的に描画可能になったタイミングのフレームを継ぎはぎする必要があるので、描画可能になったタイミングでスクリーンショットを撮る実装の方がシンプルです。 ↩︎