【Remotion】字幕だけ変えたいとき、全部描き直さずに済むか【3方式を実測】


こんにちは、フリーランスエンジニアのmohです。この記事はほとんどAIが書いたものを、私が加筆修正しています。検証不十分な部分もあるかと思いますが、ご容赦ください。ご指摘等ございましたら、Github issueか、Xでお願いいたします。

Remotion でサーバー側のレンダーを回していると、「字幕の ON/OFF を変えただけなのに、動画を全部描き直している」という場面が出てきます。背景の動画に透過 webm の人物を重ねて、その上に字幕を載せる構成だと、字幕は最前面のレイヤーにすぎません。それなら、字幕なしで作った完成動画を取っておいて、字幕だけあとから足せば安く済むのではないか、と考えました。

足し方は 2 つ思いつきます。完成動画を Remotion に 1 本の動画として読ませて字幕を描き足す方法と、字幕だけを透過動画として Remotion に描かせて ffmpeg で重ねる方法です。どちらがどれだけ速いのかを、GCE の 4 vCPU の VM で測りました。

結論

  • 全部を描き直す場合を 100% とすると、完成動画に字幕を描き足す方法は 79%、字幕だけ透過で描いて ffmpeg で重ねる方法は VP9 で 71%、ProRes 4444 で 84% でした。どの方法でも 2〜3 割しか下がりません
  • 動画を 1 本も読まない「字幕だけ」のレンダーでも、動画 1 秒あたり 5.6〜6.5 秒かかりました。重いのは動画の読み込みではなく、全フレームを Chromium で描いてキャプチャし、透過つきでエンコードする部分です
  • 透過 webm 1 本を重ねるコストは、動画 1 秒あたり約 1.9 秒で、全体の 2 割でした
  • 字幕を描くこと自体のコストは、ほぼゼロでした
  • @remotion/media の <Video> で透過 webm を描くと、GPU のない Linux ではソフトウェア WebGL で動き、OffthreadVideo の 2.2 倍遅くなりました
  • ProRes と VP9 のどちらが速いかは、環境で入れ替わります。macOS では ProRes が VP9 の約 1/5 の時間でしたが、Linux の VM では ProRes のほうが遅くなりました

比べた方式

素材は全部 ffmpeg で作りました。背景はゆっくり動くグラデーションの h264、人物の代わりは縁をぼかした円が動く VP9 アルファつきの webm です。どちらも 1080x1920、30fps、60 秒です。字幕は自作のコンポーネントで、行ごとの表示、単語のハイライト、幅に合わせたフォントの縮小を入れています。全方式で同じコンポーネントを使います。

記号内容
X背景 + 透過 webm + 字幕を、Remotion で全部描く (h264)
PX から字幕を抜いたもの。これが「字幕なしの完成動画」で、Y と Z の土台になる
YP を動画 1 本として Remotion に読ませ、字幕を描き足す (h264)
Z-vp9字幕だけを透過の VP9 で描き、ffmpeg の overlay で P に重ねる
Z-prores同じことを ProRes 4444 でやる
W背景 + 字幕 (透過 webm なし)。X との差が、透過 webm のコストになる
X-videoX の透過 webm を、OffthreadVideo の代わりに @remotion/media の <Video> で描く

X と P の透過 webm は、OffthreadVideo の transparent で描いています。Z の透過出力は、imageFormat: "png" に、VP9 なら pixelFormat: "yuva420p"、ProRes なら proResProfile: "4444" と pixelFormat: "yuva444p10le" を組み合わせます。

同じ時刻のフレームを X、Y、Z-vp9、Z-prores の順に並べた画像。紺色のグラデーションの背景に、縁がぼけたオレンジの円が重なり、下に「今日は朝から天気が良くて、散歩に出かけました。」という字幕が出ている。「天気が」の部分だけ黄色でハイライトされている。4 枚とも、字幕の位置と大きさ、円の縁の混ざり方が一致している

4 つの方式の出力から、同じ時刻のフレームを抜いて並べたものです。見た目は一致しています。

計測の環境

  • GCE の n2-standard-4 (4 vCPU、16GB、Intel Xeon 2.80GHz)、Debian 12
  • bun 1.4.2、Remotion 4.0.459、Chrome Headless Shell (Chromium 149)
  • ffmpeg は、overlay に apt の 5.1.9 を使いました。Remotion の内部のエンコードは、同梱の ffmpeg です
  • bundle は 1 回だけ作って、全条件で使い回しています。ブラウザも使い回しています。ただし、字幕のフォント (Noto Sans JP の 121 サブセット) の取得は、レンダーのたびに走っていて、計測値に含まれます。5 秒の尺でも 60 秒の尺でも X の秒/秒は同じ (9.13 と 9.14) だったので、この固定費は無視できる大きさだと判断しました
  • 値は「動画 1 秒あたりの処理秒数」です。小さいほど速いです
  • concurrency は、1 と Remotion の既定値の両方で測りました

同じ構成の VM を 3 台立てて、条件を分けて並列に回しています。台ごとの差を見るために、X (60 秒、concurrency 1) を 3 台で測りました。9.13〜9.20、9.03、8.94 で、差は 3% 以内でした。

最初は手元の Mac で測るつもりでした。ただ、計測中はほかの作業を止める必要があります。全条件で数時間かかる見積もりになったので、GCE に移しました。サーバーで回す想定の数字が取れたので、結果的にはこちらのほうがよかったと思います。

結果

60 秒の尺で、各条件を 3 回ずつ測った中央値です。括弧内は最小と最大です。

条件concurrency 1既定値
X9.14 (8.94〜9.20)6.70 (6.69〜6.71)
P9.25 (9.17〜9.28)6.73 (6.71〜6.77)
W7.26 (7.25〜7.26)5.27 (5.26〜5.28)
Y7.26 (7.23〜7.28)5.30 (5.28〜5.32)
Z-vp9 描く5.60 (5.59〜5.61)4.26 (4.23〜4.27)
Z-vp9 重ねる0.920.92
Z-prores 描く6.54 (6.54〜6.57)5.12 (5.09〜5.14)
Z-prores 重ねる1.181.18
X-video (n=1)20.4017.83

回ごとのばらつきは 1% 前後で、ほとんどありませんでした。

字幕つきの完成動画を得るまでの合計を、concurrency 1 で並べるとこうなります。

作り方秒/秒X との比
X: 全部を描き直す9.14100%
Y: 完成動画に字幕を描き足す7.2679%
Z-vp9: 字幕だけ描く + 重ねる6.5271%
Z-prores: 字幕だけ描く + 重ねる7.7284%

長い尺でも測りました。concurrency は既定値で、各 1 回です。

条件尺秒/秒60 秒のときの値
Y20 分6.925.30
Z-prores 描く5 分5.075.12
Z-prores 重ねる5 分1.161.18

Z-prores は、尺にきれいに比例しました。Y は、20 分にすると 60 秒のときより約 3 割遅くなっています。原因は調べていません。

考察

重いのは「全フレームを描いてキャプチャする」部分

いちばん意外だったのは、字幕だけのレンダーが速くなかったことです。Z は動画を 1 本も読みません。描くのはテキストだけです。それでも動画 1 秒あたり 5.6〜6.5 秒かかります。X の 9.14 秒のうち、6 割以上が「Chromium で 1 フレームずつ描いて、キャプチャして、エンコードする」という土台の部分だったことになります。

内訳を引き算で出すと、こうなります。

  • X − W = 約 1.9 秒。透過 webm 1 本を OffthreadVideo の transparent で読むコストです
  • X と P はほぼ同じです。字幕を描くコストは、計測の誤差に埋もれます
  • W と Y は同じ 7.26 秒でした。背景の動画を読むのも、完成動画を読むのも、h264 を 1 本読むことに変わりはありません

Y で消えるのは、透過 webm 1 本ぶんだけです。重ねる透過 webm が 3 本、4 本と増える構成なら、Y の効果はそのぶん大きくなるはずです。今回は 1 本でしか測っていません。

透過出力は、キャプチャもエンコードも重い

Z が Y より大きく速くならないのは、透過で出すための条件が重いからだと見ています。透過出力では、フレームのキャプチャが JPEG ではなく PNG になります。コーデックも、アルファつきの VP9 か ProRes 4444 になります。

ProRes の 5 分の計測中に、VM の上でプロセスを観察しました。Remotion は、フレームのキャプチャを先に進めて、エンコードはあとから ffmpeg が追いかけます。全フレームのキャプチャが終わった時点で、出力ファイルは最終サイズの 3 割ほどでした。残りの時間は、ffmpeg が 4 コアを使い切ってエンコードを続けていました。この VM では、ProRes 4444 のエンコードが律速になっています。

ProRes の中間ファイルは、60 秒で 269MB、5 分で 1.35GB になりました。20 分なら 5GB を超えます。ディスクと転送のコストも、無視できません。

ProRes と VP9 の順位は、環境で入れ替わる

本番の計測の前に、手元の Mac (Apple Silicon) で 5 秒の動作確認をしていました。そのときは、ProRes の描画が 8.6 秒、VP9 が 41.1 秒で、ProRes が約 1/5 の時間でした。それを見て ProRes が本命だと考えていたのですが、Linux の VM では逆の結果になりました。

手元の結果でコーデックを決めて、本番に持っていくと外れます。透過出力のコーデックは、本番と同じ環境で測って決めるのがよさそうです。

<Video> は GPU のない環境では遅い

@remotion/media の <Video> は、アルファの合成に WebGL2 を使います。GPU のない Linux の VM では、ソフトウェア描画の WebGL で動いて、OffthreadVideo の 2.2 倍の時間がかかりました。Mac では、headless の Chromium で WebGL2 が取れず、OffthreadVideo に自動でフォールバックしました。どちらも、エラーにはなりません。黙って遅くなるか、黙って別の経路になります。

はまりどころ

計測の準備で、ドキュメントに見当たらない挙動に 3 つ当たりました。

  1. OffthreadVideo や <Video> の src に file:// を渡しても、読めません。フレームの取得は、renderMedia が立てるローカルサーバーの /proxy を必ず通ります。その先は、http:// か https:// しか受け付けません。素材は bundle の出力ディレクトリにコピーして、相対パスで参照しました
  2. libvpx-vp9 で -b:v 0 をつけて作った透過 webm は、OffthreadVideo の transparent で読むと、アルファが全面 0 になります。図形が消えますが、エラーは出ません。ffmpeg のデコーダでは、正しく読めるファイルです。Remotion が VP9 アルファを書き出すときの内部のコマンドは、-b:v をつけずに -crf だけを使っています。それに合わせたら、読めるようになりました
  3. ffmpeg の overlay に透過の VP9 を読ませるときは、入力側に -c:v libvpx-vp9 を明示しないと、アルファが無視されます。既定の vp9 デコーダは、アルファのチャンネルを読みません。重ねる側の背景が、黒く塗りつぶされます。ProRes 4444 では、この指定は要りません

もう 1 つは、自分の実装のミスです。<AbsoluteFill> は、既定で display: flex; flex-direction: column を持っています。複数の OffthreadVideo を素の兄弟要素として並べると、重ならずに縦に並びます。2 枚目のレイヤーが画面の外に出て、見えなくなりました。レイヤーを 1 つずつ <AbsoluteFill> で包めば、重なります。

限界

  • 素材は合成の図形で、人物の動画ではありません。デコードのコストは解像度とコーデックで決まると考えていますが、確かめてはいません
  • 透過 webm は 1 本だけです。本数を増やしたときの伸び方は、測っていません
  • 長い尺は、Y の 20 分と Z-prores の 5 分だけです。X と Z-vp9 の長い尺は、測っていません
  • VM は n2-standard-4 の 1 種類です。コア数やメモリが違えば、concurrency の既定値も、結果も変わります
  • 字幕にフェードなどの動きはありません

実務での扱い

「字幕だけ変えるなら安く済むはず」という見込みは、Remotion で字幕を描く限り、外れました。全フレームを描くコストが残るからです。処理時間を大きく減らしたいなら、全フレームを描かない作りにする必要があります。

字幕が変わるのは、行が切り替わる瞬間と、ハイライトが次の単語に移る瞬間だけです。その瞬間の静止画だけを Remotion で描いて、表示する時間をつけて ffmpeg で重ねれば、描く枚数は 30fps の全フレームから、単語の数の程度まで減ります。見た目は、同じコンポーネントのままにできます。これはまだ測っていないので、次に試すつもりです。

再現手順

コードと結果の JSON は、blog-examples に置いています。

bun install
bun run measure                 # 60 秒、3 回、全条件
LONG=1 bun run measure          # 長い尺も測る

尺、回数、条件、concurrency は、env か引数で上書きできます。素材は、初回に ffmpeg で生成します。全条件を回すと数時間かかるので、手元のマシンで回すときはご注意ください。

参考