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を設定し、古くなったセグメントを自動的に削除します。

ブラウザが画面を開いたときは、まず現在保存されている直近のセグメントを取得します。その後に、新しいセグメントの通知を受け取って末尾へ追加します。

通知だけを頼りにすると、ブラウザが一時的に切断していた間の動画が抜けます。通知が重複したり、順番が入れ替わったりすることもあります。そのため、次のような構成にします。

  1. 最初に直近のセグメント一覧を取得する
  2. 新着通知を受け取る
  3. 安定したIDで重複を除き、撮影時刻で並べる
  4. 時刻の飛びや通信切断を検出したら、一覧を再取得する

イベントは新着を早く知るために使い、現在の状態は保存先から取り直せるようにします。

また、ライブ表示の保存や通知が失敗しても、解析や録画を止めないようにします。誰も見ていないライブ画面のために、長期保存する動画まで失うのは困ります。

長期保存用は数十秒へまとめる

ニアライブ用の短いセグメントを、そのまま長期保存することもできます。ただ、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のエンコード処理も増えません。

FFmpegのstreamcopy: https://www.ffmpeg.org/ffmpeg.html#Stream-copy

ただし、どのファイルでも無条件に結合できるわけではありません。FFmpegのconcat demuxerは、各入力が同じstream、codec、time baseなどを持つことを前提にしています。

FFmpeg concat demuxer: https://ffmpeg.org/ffmpeg-formats.html#concat

入力の切断、解像度やコーデックの変更、時刻の後退、大きな空白があった場合は、そこで録画を確定して次のファイルに分けます。公称5秒などの固定値を足し合わせるのではなく、それぞれの実際の開始時刻と長さも記録します。

ライブと録画では、成功の意味が違う

短いセグメントを共有していても、ライブ表示と長期保存は同じ処理にはしません。

ライブ表示はbest effortです。新しい映像が少し抜けても、最新の状態へ追いつけば利用を続けられます。視聴者がいないなら、ライブ用の保存自体を止めてもかまいません。

録画は、あとから確認するためのデータです。保存に失敗した場合はローカルのspoolへ残し、再試行します。プロセスを再起動しても、未送信の録画を復元できるようにします。

短い符号化済みセグメント
        ├── live queue ── 短期保存 ── 通知
        │                 失敗時は次へ進める
        └── recording spool ── streamcopy ── 長期保存
                              成功まで保持する

符号化までは共有し、保存のqueue、保存先、保持期間、再試行、成功条件は分離します。これで、ライブ表示の遅延や通知障害を録画経路へ伝えずに済みます。

AWSの動画サービスを使う方法もある

AWSには動画を扱うマネージドサービスがあります。

Amazon Kinesis Video Streamsは、機器から動画を取り込み、保存し、ライブまたは過去の動画を再生する機能を持っています。HLSやMPEG-DASHによる再生もできます。この要件に最も近い候補の一つです。

https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/how-it-works.html

Amazon IVSは、低遅延のライブ動画を多くの視聴者へ配信し、インタラクティブな体験を作るためのサービスです。取り込み、トランスコード、世界規模の配信、専用playerまでを管理してくれます。

https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/what-is.html

AWS Elemental MediaPackageは、ライブ動画をHLSやDASHなどへ動的にパッケージし、配信元として機能します。

https://docs.aws.amazon.com/mediapackage/latest/userguide/what-is-terms.html

これらは、多数の視聴者、複数の画質、標準的な配信プロトコル、広い端末対応、配信基盤の運用削減が必要な場合には有力です。

一方、このプロトタイプでは、GPU上ですでに解析後の動画をエンコードしており、視聴者は少人数です。必要なのは世界規模の映像配信ではなく、保存した短い動画を少し遅れて確認することでした。解析結果との時刻同期や、同じデータから長期録画を作ることも必要です。

その場合は、短期保存、オブジェクト作成通知、CDN、ブラウザのMSEを組み合わせた方が、必要な部分だけを作れることがあります。

ただし、視聴者数が増える、複数画質が必要になる、端末やブラウザの種類が増える、もっと低遅延にしたい、配信の監視運用を自分で持ちたくない、といった条件になれば、マネージドサービスをあらためて選ぶ方がよいでしょう。

GPUが対応しているコーデックだけでは決められない

NVENCやNVDECが対応しているコーデックでも、ブラウザが同じコーデックをMSEで再生できるとは限りません。入力カメラ、GPU、コンテナ形式、ブラウザ、利用する端末のすべてを通せるコーデックを選ぶ必要があります。

圧縮率だけを見れば別のコーデックが魅力的でも、利用するブラウザの一部で再生できなければ、確認画面として使えません。MediaSource.isTypeSupported()による確認に加えて、対象にする実機で再生を試す必要があります。

まとめ

小規模なニアライブ確認と長期録画が必要なら、GPUで解析後の動画を一度だけエンコードし、その結果を短い自己完結セグメントとして扱う方法があります。

  • ニアライブは短いセグメントをそのまま短期保存して、確定したものから表示する
  • 長期録画は同じセグメントをstreamcopyで数十秒へまとめる
  • ライブはbest effort、録画は再試行可能な永続spoolとして分離する
  • イベント通知だけを信頼せず、現在のセグメント一覧から再同期する
  • 視聴者や配信要件が増えたら、マネージド動画サービスを再検討する

大切なのは、ライブ用と録画用に動画を二度作ることではなく、一度作った符号化済みの動画を、異なる寿命と成功条件で使い分けることだと思います。