GPUで一度だけ動画をエンコードして、ニアライブ表示と録画を作る
この記事は、筆者が構成を決め、内容と参考資料を確認したうえで、構成の整理と本文の作成にLLM(OpenAI Codex)を使用しています。記事内容の責任は筆者にあります。
動画ストリームを解析する、小さなプロトタイプを作るとします。
カメラから届く映像をGPUでデコードし、物体の検出などの処理をします。解析した結果は数値や座標として保存しますが、あとから結果を確認するために、解析後の動画も保存しておきます。
さらに、処理が正しく動いているかを確認するため、現在の映像もブラウザで見たくなりました。ただし、不特定多数へ映像を配信するサービスではありません。システムを設定した人が、ときどき画面を開いて状態を確認する程度です。視聴者は少なく、数秒の遅れも許容できます。
このような用途で、ライブ表示用と長期保存用の動画をどう作るかを考えます。
GPUでのエンコードは一度だけにする
NVIDIAのGPUには、動画をデコードするNVDECと、エンコードするNVENCがあります。デコードしたフレームをGPU上で処理して、専用のエンコーダへ渡せます。
NVIDIA Video Codec SDK: https://docs.nvidia.com/video-technologies/video-codec-sdk/13.1/index.html
入力された動画を解析するだけなら、元の圧縮データをそのまま保存する方法もあります。しかし、検出対象にマスクをかけたり、表示用に解像度を変えたりする場合は、変更後のフレームをもう一度エンコードする必要があります。
このエンコードを、ライブ表示用と録画用で別々に行う必要はありません。
カメラの動画ストリーム
↓
GPUでデコード
↓
解析・画像処理
↓
GPUで一度だけエンコード
↓
短い動画セグメント
├── 短期間のニアライブ表示
└── 数本をまとめて長期保存
同じエンコード結果を使えば、ライブと録画で画質や解像度が違ってしまうこともありません。画像にマスクをかける場合も、分岐する前に一度だけ処理するため、一方の経路だけマスクを忘れる事故を避けやすくなります。
数秒の自己完結した動画へ分ける
エンコード結果は、例えば5秒から10秒程度の短い動画へ分けます。長さは固定値ではなく、キーフレームの位置などによって多少前後します。
ここでは、それぞれを単独で再生できるfragmented MP4にします。一つのファイルに、再生開始に必要な初期化情報と動画データを持たせます。
短いセグメントが確定するたびにオブジェクトストレージへ保存し、ブラウザへ新しい動画ができたことを通知します。ブラウザはMedia Source ExtensionsのSourceBufferへセグメントを順番に追加します。
Media Source Extensions: https://www.w3.org/TR/media-source-2/
数秒ごとにファイルが確定してから表示するため、テレビ会議のようなリアルタイム通信にはなりません。しかし、プロトタイプの処理状況を確認する用途なら、数秒程度遅れて見えるニアライブでも十分なことがあります。
ライブ表示は短期データとして扱う
ニアライブ用のセグメントは、視聴している人がいる間だけ保存します。保存先には短いLifecycleを設定し、古くなったセグメントを自動的に削除します。
ブラウザが画面を開いたときは、まず現在保存されている直近のセグメントを取得します。その後に、新しいセグメントの通知を受け取って末尾へ追加します。
通知だけを頼りにすると、ブラウザが一時的に切断していた間の動画が抜けます。通知が重複したり、順番が入れ替わったりすることもあります。そのため、次のような構成にします。
- 最初に直近のセグメント一覧を取得する
- 新着通知を受け取る
- 安定したIDで重複を除き、撮影時刻で並べる
- 時刻の飛びや通信切断を検出したら、一覧を再取得する
イベントは新着を早く知るために使い、現在の状態は保存先から取り直せるようにします。
また、ライブ表示の保存や通知が失敗しても、解析や録画を止めないようにします。誰も見ていないライブ画面のために、長期保存する動画まで失うのは困ります。
長期保存用は数十秒へまとめる
ニアライブ用の短いセグメントを、そのまま長期保存することもできます。ただ、5秒の動画を一日中保存すると、1台だけでも一日17,280個のオブジェクトになります。
オブジェクトが細かいほど、一覧取得、検索用データ、署名URL、再生時の要求数などが増えます。一方で、数分単位まで大きくすると、ファイルが確定するまでの時間が長くなり、保存に失敗したときの再送単位も大きくなります。
そこで、例えばニアライブ用の短いセグメントを数本まとめて、30秒程度の録画オブジェクトにします。30秒は動画形式から決まる値ではありません。オブジェクト数、再生時の扱いやすさ、確定までの遅れ、再送単位のバランスから決める値です。
このとき、動画をデコードして再エンコードする必要はありません。コーデック、解像度、time baseなどが同じで、時刻が連続しているセグメントなら、FFmpegのstreamcopyを使って一つのコンテナへまとめられます。
ffmpeg -f concat -safe 0 -i segments.ffconcat -c copy recording.mp4
-c copyはデコードとエンコードをせず、圧縮済みのstreamを新しいコンテナへコピーします。再エンコードによる画質劣化はなく、GPUのエンコード処理も増えません。