Denoとtsxとffmpegで無理やり動画を作る
Denoで画像を生成する記事に感化されましたが、画像さえ作れれば動画も作れます。連番で出力してffmpegでmp4にしてしまえばよいです。
素材として3枚の画像を用意しました(遊びに好きに使ってください)。
まずは、最小限の画像を生成するコードを書きます。
// main.tsx
import { ImageResponse } from "https://deno.land/x/og_edge@0.0.6/mod.ts";
async function imageToBase64(image: string) {
const data = await Deno.readFile(image);
const base64 = btoa(String.fromCharCode(...data));
const mimeType = image.split(".").pop();
return `data:image/${mimeType};base64,${base64}`;
}
const imageSize = {
width: 633,
height: 488,
};
const imageScale = 0.8;
const image = await imageToBase64("./walk-deno-1.png");
function render() {
return new ImageResponse(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 64,
background: "#f5dd42",
}}
>
<div
style={{
marginTop: 64,
fontSize: 64,
}}
>
Hello Deno!
</div>
<img
style={{
width: imageSize.width * imageScale,
height: imageSize.height * imageScale,
}}
src={image}
/>
</div>,
);
}
const res = await render();
Deno.mkdir("images", { recursive: true });
if (res.body) {
Deno.writeFile(
`images/image.png`,
res.body,
);
}
Netlifyの人がVercelのogp用の画像生成ツールSatoriをラップしたライブラリをDenoのレジストリで公開してくれています(豪華コラボ)。
Satoriは超すごく、HTMLとCSSと解釈して、テキスト等をベクター形状に変換しSVGに変換します。最後にresvgというRust製のSVGレンダラーを使ってラスター画像に変換します。ここまでJSかwasmなのでDenoでも動くというわけです。og_edgeはogp用なので当然Responseオブジェクトを返すのですが、手抜きのためにそのまま使います。render()
という関数を実行すると、pngのバイナリを詰めたResponseオブジェクトが返ってくるので、それをserveせずにストリームをファイルに接続してそのまま書いてしまいます。
では早速 deno run -A ./main.tsx
で実行します。
その結果、こんなファイルができます。
これをアニメーションさせるには、render()
をフレーム数を引数を取るようにして、render(index: number)
に変えればよいです。
indexの値に応じてちょっとずつ違う画像を返してあげればアニメーションになります。
Denoくんをジャンプさせるには、例えばこんなコードになります。
const jumpHeightMax = 96;
const y = Math.abs(Math.sin(index * Math.PI * 2 / 16)) * jumpHeightMax;
const transform = `translateY(${-16 - y}px) scale(${
y / jumpHeightMax / 5 + 1
})`;
最後のtransformにはCSSプロパティのtransformに渡す文字列を生成しています。基本的にこういったアニメーションを作るときはabsoluteや素のwidth, heightで頑張るのではなく、transformを駆使すると、レイアウトに影響が及ばないので良いです。
Math.sinを使っているのは、こういう感じの波形を出してくれるからですね。
この絶対値を取って、かまぼこ状にジャンプさせています。
周期は(Math.PI * 2 / フレーム数)
のように書きます。
あとのごちゃごちゃしたオフセットの値は、目視で確認しながら調整しています。
あとは、連番画像をゴリゴリ出力するだけです。
for (let i = 0; i < 64; i++) {
const res = await render(i);
Deno.mkdir("images", { recursive: true });
if (res.body) {
Deno.writeFile(
`images/image_${i.toString().padStart(2, "0")}.png`,
res.body,
);
}
}
大量にできました。
さすがに64枚ともなると2.5秒くらい生成にかかってます(M2 Mac mini)
なお、deno run --watch
を使うと、コードを変更したらすぐVSCodeのイメージプレビューに反映されるため微調整に便利です。
ファイルツリーをタイムラインに見立てれば、もうこれは動画編集ツールだと言っても過言ではないですね!(過言です)
あとはffmpeg用のタスクを書いて、実行すればmp4が手に入ります。
"tasks": {
"mp4": "ffmpeg -framerate 16 -i images/image_%02d.png -vcodec libx264 -pix_fmt yuv420p -r 60 out.mp4"
},
完成です!
よいDenoライフを!
余談
コンセプト的にはRemotionに影響を受けています。こっちはタイムラインUIがあったり分割レンダリングができたりと豪華な作りになっています。
でもなにげに、今回のDeno版Remotionもシンプルな割に有用な可能性があると思います。やはり簡単にTSXが扱えるところが強いですね。その他、TSXでなにかDenoにやらせるみたいな用途は普通に便利だと思います。Satoriはちょっとレイアウト面でflexしか使えないなど制約はあるのですが…。
MotionCanvasも同じようなコンセプトですね。
DenoでReact + TSXを扱う設定は下記記事を参考にしました。deno.jsonだけ置いたらすぐにTSX書いて実行できます。
Denoでffmpegのwasm版を動かして、完全にDeno上で動画を生成したかったのですが、今のDenoで動かすことができた人はまだいないようです。ネイティブのffmpegのほうが動作が速いのであえてwasmでやるメリットも大きくないのですが、将来的には動いてほしいかな〜と思っています。
少し完成品をいじってどこまで凝ったものが作れるか試していました。その結果が記事先頭のGIFなのですが、サボテンを追加したところ目に見えてレンダリングが遅くなりました。何が効いてしまってるのかは詳しく調べていませんが、画像のスケーリングあたりは重くてもおかしくないかなと感じました。あとはtranspose: scaleのレンダリングがガタつく気がして除去しています。
やはりこの手のレンダリングはGPUに任せるのが理想でしょうね。DenoはWebGPUを一度実装していたことがあるのですが、諸般の事情により取り下げになっています。再度DenoにWebGPUを載せようというPRが動いていますが、こういったGPU資源を使えるような状況が訪れれば、Denoで動画編集ツールが実装されることもありうるのかもしれません。
Discussion