Remotionっていう「Reactで動画を作れるパッケージ」があって、すごく面白かったからその紹介をするよ。
記事中の画像はみんな、自分が Remotion で作った動画からのスクショなんだ。その動画(の縦長版)はこれ
Remotionのこと1つずつ書いていくね。
Remotionってなに?
公式ドキュメントはこれ。
Remotionでは、JSXとCSSで書いた画面が1フレームずつ描画されて、最終的にmp4になる。
<Composition id="MyVideo" component={MyVideo} durationInFrames={150} fps={30} />
これで、30fpsで150フレーム...つまり5秒の動画の枠組みができた。あとは MyVideo を普通のReactコンポーネントとして書けば好きな動画を作れる。
コンポーネント設計がそのまま動画の構成になるから、タイトルや本編、アウトロを部品に切って組み合わせられる。React で Web アプリケーションを書くみたいに動画を作れるよ ( なんなら Web アプリケーションを書くみたいにというか、Web アプリケーションが動画になると言った方が正しい )
アニメーションをどう書くか
シンプルなアプローチ。
useCurrentFrame() で「今が何枚目か」を取る。それだけだよ。
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 30, 120, 150], [0, 1, 1, 0]);
const x = interpolate(frame, [0, 150], [-140, 140]);
interpolate は入力と出力の配列を対応づけて補間してくれる関数だね。配列を伸ばせば、フェードイン→ホールド→フェードアウトが1本で書けるよ。
見ての通り、stateもランダム性も使っていない。同じframeなら、何度再生しても必ず同じ絵になるんだね。
バネっぽい動きが欲しいときは spring() だよ。damping が小さいほど大きく跳ねる。
spring({ frame, fps, config: { damping } })
左から damping: 4 / 12 / 30。4は1.5倍あたりまで行き過ぎてから戻ってきて、30は行き過ぎずに寄っていくね。変えたのは数値ひとつだけだよ。
複数シーンをどう並べるか
さっき「タイトル」「本編」「アウトロ」と書いたけど、動画ってたいてい、こういういくつかのシーンでできているよね。じゃあ、それをどう並べるか。
<Series> の中に <Series.Sequence> を置いて、それぞれの長さを書くだけでいいよ。
<Series>
<Series.Sequence durationInFrames={60}><TitleScene /></Series.Sequence>
<Series.Sequence durationInFrames={90}><MainScene /></Series.Sequence>
<Series.Sequence durationInFrames={60}><OutroScene /></Series.Sequence>
</Series>
Sequenceの中では useCurrentFrame() が0から始まる。 CSSの position: relative の時間版だと思ってもらえるといいかな。それぞれのシーンは、自分が動画全体のどこに置かれているかを知らないまま書ける。
この仕組みは、あとからシーンの長さを変えたくなったときに便利。動画作りって、その「ここちょっと伸ばしたい」を延々やる作業なんだよね。
もし動画全体の通し番号でフレームを指定していたら、前のシーンを60フレーム伸ばした瞬間に、以降のシーン全部の数値を書き換える羽目になる。Sequenceなら子のコードに影響が跳ねたりしない。
あとSequenceは、範囲外のシーンをアンマウントしてくれる(出番前の <Audio> が勝手に鳴り出さない)し、重ねることもできる(2つのシーンが同時に存在できるから、クロスフェードが成立する)。「今のframeがこの範囲ならこのシーンを出す」と自前のif文で書き分けていたら、この2つは手に入らないよ。
書き出しって、何をやってるの?
ここまでで動画は組めた。じゃあそれが、どうやってmp4になるんだろう。
ここもすごくシンプルなアプローチを採用してるんだよね。ブラウザで開いて、1フレームずつスクリーンショットを撮っているだけ。最後にffmpegで繋いでmp4になる。
素朴だよね。でもこの素朴さのおかげで、ブラウザが描けるものは全部そのまま映像になるんだ。CSS、SVG、Canvas、WebGL、Web Font...つまり Remotion における表現力の限界は、ブラウザの表現力の限界そのものとほとんど同義。新しいCSSが使えるようになったら、その日から動画でも使えるよ。
ただ、ひとつ落とし穴がある。それは、実時間は流れていないということ。
1フレームずつ「座らせて」撮っているから、CSSの transition も setInterval も進まない。普段の感覚でCSSアニメを書くと、書き出した動画は完全に止まった絵になるよ。
だから時間は必ず useCurrentFrame() から取ろう。
動画の書き出しに Lambda を使える
さっき「同じframeなら必ず同じ絵になる」と書いたよね。これはつまり、500枚目を作るのに499枚目の結果がいらないということだよ。
順番に作る必要がない。バラバラのマシンで同時に作れる。
だから例えば、Lambda を大量に起動して、並列で一気に動画の書き出しをすることもできる。
Remotion自身がそのための パッケージ 「Remotion Lambda」を出している。
動画をchunkに割って、Lambdaに散らして、最後に繋いでくれるよ。
上の動画におけるオレンジ色のラインが「1台で順番に」動画の書き出しを行った時のイメージ。一方で、その下の6本が並列に散らしたchunkだよ。並列側が全部終わって結合まで済んでも、1台のほうはまだ先が長いね。
呼び出し方はこんな感じ。書き出しを投げて、あとは進捗を読むだけだよ。
const { renderId, bucketName } = await renderMediaOnLambda({ /* ... */ });
// 進捗を読むのはこう(リアルタイムでレンダリング状況を書き出したいならこれをポーリング)
const p = await getRenderProgress({ renderId, bucketName });
p.overallProgress; // 0 → 1
Lambdaの台数を増やせば、尺が伸びても書き出し時間をほぼ横ばいにできるよ。
※ただし、各chunkは参照アセットを個別にダウンロードするからね。200並列なら同じ画像を最大200回取りに行くことになるから、外部アセットはCDNに置こう。自分で自分にDDoSを仕掛ける羽目になるからね。
Remotion Lambda において、Lambdaには何がデプロイされてるの?
Lambdaが全てを担うわけじゃない。デプロイは2つに分かれているんだ。
-
deployFunction()… 汎用レンダラー(エンジン + ヘッドレスChrome)。動画そのもののコードは1行も入っていない -
deploySite()… S3上の静的サイト。バンドルされた自分の動画コード
レンダラーがそのURLをブラウザで開いて、1フレームずつ撮る。さっきの「連続スクリーンショット」が、そのままクラウドに載っただけだね。
仮に前者と後者が一体化してたら、動画を1行直すたびにLambdaの再デプロイが必要になっちゃってすごく手間だよね。でも、分かれていれば、動画側の変更はS3に上げ直すだけで済む。実用的だよね。
まとめ
- 時間は
useCurrentFrame()から取るよ。CSSのtransitionもsetIntervalも書き出し時には進まないから、そこだけは普段のWeb開発と違うから気をつけてね -
Sequenceの中ではuseCurrentFrame()が0起点になるよ。おかげで、尺をいじるたびに全部書き直す羽目にならずに済む - 書き出しはブラウザの連続スクリーンショットだよ。だからWeb技術がそのまま映像になる
- フレームが独立しているから、Lambdaに並列で散らせるよ。ただし外部アセットはCDNに置いてね
- Remotion Lambda でレンダリングさせる時、その Lambda には動画コードが入っていないよ。動画を直したときは
deploySite()してからレンダリング.
あと、企業で使うならライセンスだけ先に見ておいてね。個人・従業員3名までの営利企業・非営利なら無料だけど、それを超える規模の営利企業は企業向けライセンスが必要だよ。





