中華製ドラレコアプリがRosetta廃止で使えなくなるので、自分でmacOSアプリを作った話
- ⏱ 約8分で読めます - 👁 … views車のドライブレコーダーのSDカードを見返すたびに、付属の再生ソフトを起動するのが憂鬱でした。中華製OEM機(“vsys a6l"系)のビューアはmacOS用バイナリこそあるものの、Intel版しかありません。今はRosettaで動いていますが、macOS 26以降はRosettaによるIntelバイナリのサポート自体が終了し、Apple Siliconネイティブのバイナリしか動かなくなります。つまり「そのうち使えなくなる」ではなく「次のOSアップデートで確実に使えなくなる」段階に来ていました。そろそろ自分で作ったほうが早い、と判断しました。
何が不満だったか
このドラレコはフロント・リア2カメラで、SDカードにMPEG-TS(.TS)形式で録画されます。付属ソフトは動画を見るだけなら問題ないんですが、次の点がずっと引っかかっていました。
- Apple Siliconネイティブバイナリがなく、Intel版をRosettaで動かしている
- macOS 26以降、RosettaによるIntelバイナリのサポートが終了する予定で、次のOSアップデートで確実に起動しなくなる
- フロント・リアを同時に、しかも同期させて見られない
- GPS・速度・加速度センサーの情報が動画とバラバラにしか見られない
Claude Codeと相談しながら、まずTSファイルの中身を解析するところから始めて、最終的にSwift/SwiftUI製のネイティブmacOSアプリまで作り切りました。
TSファイルは変換不要でそのまま再生できた
一番の発見は、このTSファイルがAVFoundationで直接再生・シークできるということでした。てっきりffmpegでMP4にremuxしないと扱えないと思い込んでいたんですが、実機で検証したら普通にAVPlayerに食わせられました。おかげで設計がかなり単純になりました。
swift run DriveRecorderViewer --verify <TSファイルパス>
というCLI検証モードを最初に作って、GPS・Gセンサー抽出とAVFoundationでの再生・シークを机上ではなく実測で確認しながら進めました。
GPS・Gセンサーは独自形式で埋め込まれている
このドラレコは、動画とは別にMPEG-TSのプライベートPIDにGPS・加速度センサーのデータを埋め込んでいます。
- PID
0x01e4:LIGOGPSINFO形式。132バイト固定ブロックで、バイト単位のビット演算で難読化されている - PID
0x0e1a:$GSENSORD形式。ASCII平文で加速度センサーのオフセット値が入っている
難読化の仕様書はどこにもないので、まずPythonでリバースエンジニアリングしてパーサーを書き、それをSwiftへ移植しました。移植の正しさは、両方の実装に同じファイルを食わせて出力を突き合わせる検証スクリプトを書いて、16ファイルで完全一致することを確認しています。
地味にハマったのが、Gセンサーの時刻同期です。専用ストリームのGセンサー値にはタイムスタンプが付いていないので、「ファイル名から取れる録画開始時刻」をアンカーにして、録画の総尺(約120.33秒)で等分し、逆算でUTC秒軸に変換しました。GPS側の最初のレコード時刻を起点にすると、GPSモジュールの起動遅延の分だけずれてしまうので使えません。
フロント・リアの同期が一番の技術的な山場だった
複数カメラの動画を同期再生するだけなら簡単そうに見えて、実際は結構やっかいでした。
最初はフロント・リアそれぞれのAVPlayerに個別にrateを設定する素朴な実装にしていました。動くには動くんですが、倍速再生をすると微妙にズレが蓄積していきます。ズレを補正しようとシークを入れると、今度はシークの頻度と精度のトレードオフに悩まされました。補正を厳しくすればシークが連発してカクつくし、緩めれば同期がズレて見える。この二択から抜けられませんでした。
最終的にはAVPlayer.setRate(_:time:atHostTime:)というAPIで、フロント・リア両方のタイムベースを同じホストクロックのアンカー時刻に固定する方式に変えました。同じ基準時刻・同じ動画内時刻・同じレートで両方を同時に走らせれば、そもそもズレが発生しません。高速再生でデコードが追いつかずコマ落ちすることはあっても、時間軸そのものはズレないので、体感の同期感がかなり改善しました。実用上は2倍速くらいまでが快適な上限です(2カメラ同時デコードなので、倍速×2カメラで実時間の4倍のデコード負荷になります)。
画面のレイアウトは「動画は大きく、データは1画面に」
UIは何度か作り直しました。最終的に落ち着いたのは、上部に時刻・速度・高度・進行方位を大きな数字で表示し、その下にフロント・リアの動画を横並びで画面幅いっぱいに、さらにその下に地図(正方形)とセンサーグラフ(速度・高度、加速度X/Y/Z)を並べる構成です。
地図はApple純正のMapKitを使っています。走行軌跡をポリラインで描画し、現在位置にマーカーを追従させます。ここでも1つバグを踏みました。地図オーバーレイの再構築判定を「録画のID」で行っていたんですが、録画IDはリストをクリックした瞬間に切り替わる一方、対応するGPSデータは非同期解析の完了後にしか届きません。この2つが一瞬だけズレるタイミングがあり、「IDはもう新しいのに中身は古いまま」という状態を「更新済み」と誤認して、以降ずっと軌跡が表示されない不具合になっていました。判定基準を「実際に描画するデータの中身(件数・始点・終点の座標)」に変えて解消しています。
複数ファイルを選んだら自動で連続再生
SDカードには数分単位で細切れになった録画がたくさん溜まっています。1本ずつ選び直すのが面倒だったので、複数ファイルをまとめて選択すると再生リストとして扱われ、1本が終わると自動的に次の録画へ進む連続再生に対応しました。
次に再生する録画のGPS/Gセンサー解析と動画の事前ロードは、今再生している最中にバックグラウンドで済ませておく先読み機構を入れています。切り替わりの瞬間に待たされることはありません。ファイルを開いた時点でリスト全体を低優先度で順次解析しておく一括先読みも追加していて、リストのどこをクリックしても解析待ちが出ないようにしています。
パーサーの高速化で詰まった話
TSファイルのデマルチプレクス(該当PIDのパケットを取り出す処理)は、最初188バイトずつFileHandle.readを呼ぶ素直な実装にしていました。動くけれど遅い。実測したら、79MBのファイルで約42万回のシステムコールとパケット単位のヒープ確保が支配的コストになっていて、実行時間の内訳を見るとsysタイムがuserタイムを上回っている状態でした。
ファイル全体を一括で読み込み(Data(contentsOf:options:.alwaysMapped))、メモリ上を188バイトストライドでポインタ走査する実装に変えたところ、1.14秒かかっていた処理が0.02秒まで縮みました。約50倍です。
SwiftPM実行バイナリでウィンドウが開かない
swift runや.build/debug/配下の実行バイナリを直接叩いても、macOSのウィンドウが一切開かないという現象にも遭遇しました。調べると、SwiftPMが生成する実行バイナリはInfo.plistがバンドルに正しくバインドされておらず、adhoc署名の状態ではNSApplicationがウィンドウを開けない、という仕様上の制約でした。
.appバンドルの形に組み立ててcodesign --force --deep --sign -で再署名すると解決します。ビルドスクリプト(build_app.sh)にこの手順を組み込んで、.app化と署名まで自動化しました。
できたもの
- フロント・リア動画の同期再生(共通クロックアンカー方式)
- GPS軌跡のMapKit表示、現在位置マーカーの追従
- 速度・高度・加速度(Gセンサー)のリアルタイムグラフ
- 複数ファイルの自動ペアリング(フロントだけ選んでもリアを自動探索)
- 複数録画の連続再生と先読みキャッシュ
- 0.5×〜8×の可変速再生
Swift/SwiftUI + AVFoundation + MapKit、外部プロセス(ffmpeg等)への依存なしの単体アプリです。単体テストも14件用意していて、パーサーの正しさは既存のPython実装との出力突き合わせで検証済みです。
中華製OEMドラレコがRosetta廃止で使えなくなるという差し迫った事情から始まった話ですが、TSファイルの独自フォーマットを解析するところから、AVFoundationの同期再生の癖まで、思ったより深いところまで潜ることになりました。同じ機種(vsys a6l系OEM)を使っている人の参考になれば。
コードは車両情報が特定できる録画データと同居しているリポジトリのため、現時点では非公開にしています。