同様にヘッドレスブラウザを用いてプログラマブルに動画を作成できる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を返すものです。これを使う場合最終的に描画可能になったタイミングのフレームを継ぎはぎする必要があるので、描画可能になったタイミングでスクリーンショットを撮る実装の方がシンプルです。 ↩︎
