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のエンコード処理も増えません。

S3とDynamoDBを二重の正本にしない

この記事は、筆者が構成を決め、内容と参考資料を確認したうえで、構成の整理と本文の作成にLLM(OpenAI Codex)を使用しています。記事内容の責任は筆者にあります。

動画ストリームを解析する、小さなプロトタイプが欲しくなったとします。

カメラから届く映像を処理して、その結果をあとから確認できるようにします。解析した動画は短いファイルに分けて保存します。最初は、生成した動画をAmazon S3へ置くだけでもよさそうです。S3のコンソールを開けばファイルは見つかりますし、必要なファイルをダウンロードすれば再生もできます。

プロトタイプが動くと、次はもう少し使いやすくしたくなります。

  • どの日に動画があるのか、カレンダーで確認したい
  • 指定した時刻の動画を続けて再生したい
  • 動画と同じ時刻のセンサー情報や解析結果を重ねて見たい
  • 機器ごとの保存量も知りたい

こうなると、S3にファイルを並べただけでは画面を作りにくくなります。S3のオブジェクト一覧を毎回すべて調べるより、動画の開始時刻、長さ、機器、保存先などをAmazon DynamoDBへ登録しておく方が、画面やAPIから検索しやすそうです。

動画本体をS3へ、検索情報をDynamoDBへ保存する構成です。ここまでは自然に思えます。ところが、この二つのどちらを正しいデータとして扱うかを決めずに作り始めると、保存や削除の失敗処理が急に難しくなります。

S3とDynamoDBへ同時には書けない

このプロトタイプで、短い動画ファイルを保存するときに、次のように処理するとします。

  1. 動画ファイルをS3へ保存する
  2. 動画の開始時刻、長さ、S3のキーをDynamoDBへ保存する

両方とも成功すれば問題ありません。ですが、S3への保存に成功したあと、DynamoDBへの書き込みに失敗することがあります。このとき、動画はあるのに検索結果には出てきません。

順序を逆にしても同じです。DynamoDBへ先に登録してからS3への保存に失敗すると、検索結果にはあるのに、再生しようとすると動画がありません。

DynamoDBには複数の項目をまとめて更新するトランザクションがあります。しかし、そのトランザクションにS3へのファイル保存を含めることはできません。S3とDynamoDBを一つの操作として、両方成功するか両方失敗するかにする仕組みではないからです。

AWSのDynamoDBトランザクションの説明: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transactions.html

保存だけなら、失敗した方を再試行すればよさそうです。ですが削除まで考えると、だんだん複雑になります。

削除しようとすると、さらに難しくなる

動画を削除するときも、S3のファイルとDynamoDBの検索情報を消す必要があります。

先にDynamoDBから消すと、S3の削除に失敗した場合に、検索できない動画が残ります。先にS3から消すと、DynamoDBの削除に失敗した場合に、存在しない動画が検索結果に残ります。

これを厳密に扱おうとすると、例えば次のような状態が欲しくなります。

  • 削除受付済み
  • S3削除中
  • DynamoDB更新中
  • 一部失敗
  • 再試行待ち
  • 削除完了

さらに、削除した動画が機器側の再送によって復活しないように、削除済みであることを一定期間記録する必要が出てくるかもしれません。大量の動画を削除するなら、どこまで処理したかを示すチェックポイントも必要です。

もちろん、このようなワークフローをきちんと作ることはできます。ただ、小さなプロトタイプで動画を保存して検索したいだけだったのに、二つのストレージを同じ状態に保つためのシステムが大きくなっていきます。

ここで、そもそもS3とDynamoDBの両方を正本として扱う必要があるのかを考えた方がよさそうです。

再生成できないものを正本にする

動画本体が失われると、DynamoDBに開始時刻やファイル名が残っていても動画を再生できません。一方、動画本体がS3にあり、そのS3キーから機器や撮影時刻を読み取れるなら、検索用の情報は作り直せます。

この場合は、次のように役割を分けると具合がよくなります。

  • S3: 動画が存在するという事実を持つ正本
  • DynamoDB: 画面やAPIから検索しやすくするためのread model

read modelは、正本を利用目的に合わせて読みやすい形へ変換したデータです。例えば、次のようなものです。

  • どの日に動画があるかを示すカレンダー
  • 連続して再生できる時間範囲
  • 機器ごとの最新データ
  • 保存件数や保存量の集計

これらは表示を速くするために必要ですが、動画本体そのものではありません。壊れたり消えたりしても、S3を調べて再生成できるようにします。

S3のキーをデータ構造として使う

再生成できるようにするには、S3のオブジェクトを見ただけで、最低限の情報が分からなければなりません。

例えば、次のような情報をS3キーやオブジェクトのメタデータへ持たせます。

  • データ形式のバージョン
  • データを生成した機器の識別子
  • 記録を開始したUTC時刻
  • 実際の記録時間
  • 同じデータを識別する安定したID

ファイル名に現在時刻と乱数だけを入れてしまうと、あとで一覧を取得しても、そのファイルが何を表しているのか分かりません。別のデータベースを読まないと意味が分からないS3キーでは、そのデータベースを正本から外せなくなります。

同じ入力から同じS3キーを作れるようにしておくことも大切です。保存処理を再試行したときに別の動画として増殖せず、すでに同じものがあれば同じ結果として扱えるようになります。

S3では、既存オブジェクトを上書きしない条件付き書き込みも利用できます。 https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html

イベントで早く反映し、あとから正本で確認する

S3へ動画を保存したあと、S3 Event NotificationsやAmazon EventBridgeを使って処理を起動し、DynamoDBのread modelを更新できます。

この方法なら、動画を保存する処理はS3への保存成功だけを確認すれば済みます。DynamoDBの更新が一時的に失敗しても、動画保存まで失敗したことにはしません。

ただし、イベントを新しい正本にしてはいけません。S3のイベント通知は少なくとも1回届くように設計されているため、同じイベントが複数回届くことがあります。また、発生した順番に届くことも保証されません。

AWSのS3 Event Notificationsの説明: https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html

nRF52840のDFUでSlot1がpermanentになり詰んだ話 - ダウングレードによるデバイスロック問題

この記事はLLM(GitHub Copilot)によって作成されました。

nRF Connect SDK 2.9で開発したnRF52840のファームウェアで、MCUboot/MCUmgrを使ったDFU(Device Firmware Update)中に、デバイスが完全にロックされてそれ以上DFUできなくなるという深刻なバグに遭遇しました。この問題は既にオープンソースコミュニティでも報告されていますが、ダウングレード特有のケースは明示的に議論されていないため、本記事で詳細を共有します。

発生した問題

症状: デバイスの完全なロック

バージョンが小さいファームウェアをDFUすると、デバイスが完全にロックされ、それ以上DFUできなくなるという問題が発生しました。

具体的な流れ:

1. Primary Slot: v1.0.2.2 (実行中)
2. DFU実行: v1.0.1.1 をアップロード
3. 再起動
4. MCUbootがバージョンチェック: 1.0.2.2 > 1.0.1.1 → スワップ拒否
5. Primary Slot: v1.0.2.2 のまま起動
6. Secondary Slot: v1.0.1.1 が pending 状態で残る
7. 次のDFUを試みる
8. MCUmgr: "There is no free slot to place the image" エラー
9. デッドロック状態 → DFU不可能

再現条件

以下の3つの条件が揃うと必ず発生します:

  1. ダウングレード防止が有効

    CONFIG_MCUMGR_GRP_IMG_VERSION_CMP_USE_BUILD_NUMBER=y
    
  2. バージョンが小さいイメージをDFU

    • Primary: 1.0.2.2
    • DFU: 1.0.1.1 (ダウングレード)
  3. PERMANENT modeで書き込み (nRF Connect appのデフォルト)

M1 Macで、Pythonのdlopenでエラーが出た時の対処

Google apiをpythonで使おうとして、エラーが出ました。M1 Macで、Pythonを使っていると、arm64eアーキテクチャのpythonで、x86_64アーキテクチャのダイナミックライブラリを読みこうもうとして、dlopen()でエラーが出ることがあります。インストールする側でバイナリを使わずビルドすれば、ビルドに数分ほど時間はかかりますが、これを解決できました。

https://stackoverflow.com/questions/69788752/on-apple-m1-grpc-google-api-import-error

pip install --no-binary :all: grpcio --ignore-installed
pip install --no-binary :all: grpcio-tools --ignore-installed

x86_64アーキテクチャで揃えるのはめんどくさすぎる

pipでインストールしても、パッケージ提供元からarm64eのアーキテクチャのライブラリがビルドされていないと、どうにもならないようです。対処方法として、

arch -x86_64 /bin/zsh

で、アーキテクチャを指定してから、pythonをインストールして、すべてx86_64アーキテクチャにするのがありますが、めんどくさすぎます。

WatchOS8でバックグラウンドリフレッシュでBLE

WatchOS8でバックグラウンドリフレッシュでBLE

WWDC2021 Connect Bluetooth devices to Apple Watch https://developer.apple.com/videos/play/wwdc2021/10005/

WatchOS8で、バックグラウンド アプリ リフレッシュで、BLEが限定的ですが、利用可能になりました。

仕組みとしては、アプリ側からペリフェラルの接続を出します。バックグラウンドでアプリがリフレッシュされている期間に、たまたまペリフェラルのアドバタイジングパケットが来れば、CoreBluetoothが、ペリフェラルと接続して、アプリとの通信を確立させます。

制限として、バックグラウンドで接続できるのは、その前にアプリがフォアグラウンドで、接続をした、あるものはペアリングもした、ペリフェラルのみです。新規のペリフェラルの発見は、バックグラウンドではできません。

アプリがリフレッシュ期間を終えて、ペリフェラルとの接続がまだ閉じていない場合は、CoreBluetoothが自動的に接続を切ります。次の、アプリリフレッシュタイミングが開始した時に、アプリはCoreBluetoothからdidDisconnectを受け取ります。アプリのリフレッシュをまたいで、切断イベントが通知されることに、注意します。

接続には、アプリがバックグラウンドにある間に、アドバタイジングパケットが来なければなりません。アプリのリフレッシュタイミングは指定ができないので、ペリフェラルは適度な頻度でアドバタイジングをし続けるしかないのですが、プレゼン中の説明では、感覚的な目安としては?2秒くらい?の間隔が1つの目安みたい?です。