きっかけ
これまで家にTapoの見守りカメラを置いていて、ライブ映像とR2への録画をしました。ただ「夜中に子供がどれくらい動いているか」を知りたいときに、タイムラプス形式でサッと確認できる仕組みが欲しくなった。
というわけで、指定した時間帯の録画をタイムラプスmp4として書き出す機能を作った。
実映像を使わない、というのが今回のポイント
録画パイプラインでは、チャンクの切れ目ごとにサムネイル用のJPEGを1枚だけ撮っている(だいたい1分に1枚)。これは元々サムネイル一覧表示用に作っていたものだが、タイムラプス用途にはこれで十分だと気づいた。数時間分でも数百枚程度のJPEGで済むので、動画を1秒もデコードせずに済む。
実装: ffmpegのconcatデマルチプレクサでJPEGを1本のmp4に
やることは単純で、指定した時間範囲に対応するサムネイルのURL一覧を集め、ffmpegのconcatデマルチプレクサ向けのリストファイルを作って渡すだけ。
file 'https://.../thumb1.jpg'
duration 0.041667
file 'https://.../thumb2.jpg'
duration 0.041667
...
file 'https://.../thumbN.jpg'durationは1/fps。concatデマルチプレクサは最後のエントリのduration指定を無視する仕様があるので、最後のフレームだけ一瞬で終わってしまう。それを避けるために最後のファイルをもう一度(durationなしで)書いておく、という小さなハックが必要だった。
cmd := exec.CommandContext(ctx, "ffmpeg",
"-y",
"-f", "concat", "-safe", "0",
"-protocol_whitelist", "file,http,https,tcp,tls",
"-i", listFile.Name(),
"-vsync", "vfr",
"-pix_fmt", "yuv420p",
"-c:v", "libx264",
"-movflags", "+faststart",
outPath,
)-protocol_whitelistにhttp,httpsを含めているのがポイントで、これによりffmpegはリストファイルに書かれたURL(R2の署名付きURL)を直接HTTPで取りに行ってくれる。事前にサーバー側でJPEGをダウンロードしてくる必要がなく、ffmpegに丸投げできる。サーバーがR2からのデータを中継しないという既存の方針(録画再生も署名付きURLへのリダイレクトのみ)とも一貫していて気に入っている。
出力の長さは「フレーム数 ÷ fps」で決まる。サンプリングが1分に1枚なので、fpsを上げれば上げるほど動画は短くなる、というのは最初直感に反して分かりにくかった点。fpsは「動きの滑らかさ」ではなく「何時間分をどれだけの秒数に圧縮するか」を決めるパラメータになる。
UI: 日時とfpsを指定して生成
再生画面に🎞ボタンでパネルを出し、開始・終了時刻とfpsを指定して「生成してダウンロード」を押すとサーバーの/timelapse.mp4エンドポイントを叩き、返ってきたmp4をそのままダウンロードさせている。
tlGenerate.addEventListener('click', () => {
const url = '/timelapse.mp4?start=' + startSec + '&hours=' + hours + '&fps=' + fps;
fetch(url)
.then(r => r.blob())
.then(blob => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'timelapse.mp4';
a.click();
});
});使ってみると
実際に夜間の数時間を数秒〜数十秒のタイムラプスにして見てみると、「何時ごろに動いたか」「寝相がどう変わったか」がひと目で分かるようになった。フル映像を早送りで確認していた頃と比べて、確認にかかる時間が減った。
もともと見守りカメラの映像を中継せず署名付きURLで直接R2からブラウザに渡す、という設計にしていたおかげで、タイムラプス機能もサーバーに重い処理を持たせずに済んだのは地味にラッキーだった。サムネイルという「ついでに撮っていたもの」が思わぬところで役に立った形だ。
経過ごとにベッドの端に偏ったりするところや授乳に連れていかれてる様子などがわかりかわいいに尽きる