aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

SDK入門⑨〜動画ダウンロードツールの作り方〜

計測データを動画プレイヤーで再生したいみなさん、

こんにちは。ソリューションアーキテクトの伊勢です。

こちらは aptpod Advent Calendar 2025 12月2日の記事です。


今回はintdashの計測データをMP4ファイルとして出力する方法をご紹介します。

はじめに

intdashのMedia Explorerでは、計測の映像をMP4ファイルでダウンロードできます。1

Media Explorer

また、Meas Converterでは、車両データ解析で利用されるMDFファイルでダウンロードも可能です。

tech.aptpod.co.jp

複数データの出力

利用用途によっては複数データを統合してファイル出力したいケースもあります。

今回は、より複雑な構成のMP4ファイルを出力してみます。

MP4ファイルとは

動画や音声をひとつのファイルにまとめて扱える形式です。

再生時には内部の映像や音声を取り出し、タイムスタンプに基づいて同期再生します。

おおまかな構成は以下のようになっています。

今回はこれらのトラックを統合(多重化)します。

  • trak (video): ビデオトラック
  • trak (audio): オーディオトラック
  • trak (subtitle): 字幕トラック
MP4 File
├─ ftyp                     # ファイルタイプ情報 (ブランド識別)
├─ moov                     # メタ情報(再生に必要な情報の塊)
│   ├─ mvhd                 # ムービーヘッダ(全体の時間情報など)
│   ├─ trak (video)         # ビデオトラック
│   │   ├─ tkhd             # トラックヘッダ
│   │   ├─ mdia             # メディア情報
│   │   │   ├─ mdhd         # メディアヘッダ(時間単位など)
│   │   │   ├─ hdlr         # ハンドラ(映像/音声の区別)
│   │   │   ├─ minf         # メディア情報
│   │   │   │   ├─ stbl     # サンプルテーブル
│   │   │   │   │   ├─ stsd # サンプル記述子(コーデック情報)
│   │   │   │   │   │   ├─ avc1 (AVC/H.264 codec box)
│   │   │   │   │   │   │   ├─ avcC (AVCDecoderConfigurationRecord)
│   │   │   │   │   │   │   │   ├─ SPS (Sequence Parameter Set)
│   │   │   │   │   │   │   │   └─ PPS (Picture Parameter Set)
│   │   │   │   │   ├─ stts # 時間情報(各サンプルのduration)
│   │   │   │   │   ├─ stsc # チャンク構造
│   │   │   │   │   ├─ stsz # サンプルサイズ
│   │   │   │   │   └─ stco # チャンクオフセット
│   ├─ trak (audio)         # オーディオトラック
│   │   ├─ tkhd
│   │   ├─ mdia
│   │   │   ├─ mdhd
│   │   │   ├─ hdlr
│   │   │   ├─ minf
│   │   │   │   ├─ stbl
│   │   │   │   │   ├─ stsd # サンプル記述子(例: mp4a)
│   │   │   │   │   ├─ stts
│   │   │   │   │   ├─ stsc
│   │   │   │   │   ├─ stsz
│   │   │   │   │   └─ stco
│   └─ trak (subtitle / others) # 他トラック (例: 字幕)
│
├─ mdat                     # メディアデータ本体(映像フレームや音声サンプル)
│   ├─ [Video Sample 1] → H.264フレーム (AVCC形式: length-prefixed NALU)
│   ├─ [Video Sample 2] → H.264フレーム
│   ├─ ...
│   ├─ [Audio Sample 1] → AACフレーム
│   ├─ [Audio Sample 2]
│   └─ ...
└─ free / udta / meta       # 追加のメタデータ(オプション)

やってみた

おおまかな構成です。

iPhoneアプリ intdash Motion で取得した計測データをもとにします。

データ種類ごとにファイルを出力し、最後にMP4ファイルに多重化します。2

  • 音声:PCMデータ → WAVファイル
  • 映像:H.264データ → バイナリファイル
  • 字幕:GNSSデータ(高度・速度・緯度経度) → SRTファイル
  • 多重化後:MP4ファイル

データフロー

出力パターン①:音声

シンプルな例として、音声ファイルをWAVファイルに出力します。3

対象のエッジと開始時刻〜終了時刻を指定して実行します。

実行:音声

youtu.be

出力パターン②:音声 + 映像

続いて、複数トラックを多重化します。音声 + 映像を出力するパターンです。

実行:音声 + 映像

多重化で警告が出ています。

これは、開始時刻〜終了時刻を直接指定したためで、動画の基準点であるIDRフレームが最初に現れるまで映像フレームが生成されず、黒一色で表示されます。 4

[h264 @ 0x157704b90] no frame!
[h264 @ 0x157704b90] non-existing PPS 0 referenced

youtu.be

出力パターン③:音声 + 映像 + 字幕

最後に音声 + 映像 + 字幕を出力・多重化する例です。

計測を指定して、計測の開始〜終了時刻のデータを対象とします。

字幕の緯度経度を逆ジオコーディングで住所に変換するため、Google Geocoding APIのAPIキーを指定しています。

実行:音声 + 映像 + 字幕

youtu.be

起動オプション

データ取得オプション
  • --api_url required:サーバーURL
  • --api_token required:APIトークン
  • --project_uuid:プロジェクトUUID(省略時は Global Project)
  • いずれか required
    • 計測指定
      • --meas_uuid:計測UUID
    • エッジ+時間範囲指定
      • --edge_uuid:エッジUUID
      • --start:開始時刻、RFC3339形式
      • --end:終了時刻、RFC3339形式
出力オプション
  • --outdir:出力先ディレクトリ(省略時は ./out
  • --tracks:出力するトラック audio, video, subtitle を指定(複数可、省略時はすべて)
  • --fps:映像の入力フレームレート(映像出力時、省略時は15)
  • --gmap-api-key:Google Maps API Key(字幕出力時、逆ジオコーディングに使用、省略時は緯度経度を出力)
多重化オプション
  • --mux:WAV / H.264 / SRT を MP4 に多重化(省略時は多重化なし)

サンプルプログラム

多重化にはSDK入門③でインストールしたffmpegを利用しています。

クラスアーキテクチャ

データ取得

intdash REST APIからのデータ取得はメモリ消費を抑えるため、チャンク転送エンコーディングでリクエストしています。

        api = measurement_service_data_points_api.MeasurementServiceDataPointsApi(
            self.client
        )
        params: dict[str, object] = {
            "project_uuid": self.project_uuid,
            "name": self.meas_uuid if self.meas_uuid else self.edge_uuid,
            "time_format": "ns",
            "_preload_content": False,  # 全データロードの抑止
        }
        if self.start:
            params["start"] = self.start
        if self.end:
            params["end"] = self.end
        if self.data_id_filter:
            params["data_id_filter"] = self.data_id_filter
        stream = api.list_project_data_points(**params)

データ変換

Motionで収集した音声を単純に繋げるだけだと、zzzzやppppと聞こえるジッタノイズが入っているため、除去します。

PCMデータサンプル間の時間揺らぎを等間隔サンプリングします。

                f32 = decode_pcm_s16le(data_bytes)  # bytes -> float32
                f32_out = self.resampler.push_block(t_rel, f32)
                if f32_out.size:
                    i16 = encode_pcm_s16le(f32_out)  # float32 -> bytes

データ出力

映像と音声はメモリに溜め込まず、1件ずつ出力しています。

            if data_name == "1/pcm":
...
                    self._wav.write(i16)
...
            elif data_name == "1/h264":
...
                self._h264.write(data_bytes)


また、字幕はGNSSの複数項目を統合して生成します。

高度・速度・緯度経度・住所の字幕セグメントに集約します。

            elif data_name == "1/gnss_speed"
...
                    aggregator.update_speed(v)
...
            elif data_name == "1/gnss_altitude":
...
                    aggregator.update_altitude(alt)
...
            elif data_name == "1/gnss_coordinates":
...
                    changed, lat_q, lon_q = aggregator.update_latlon(lat, lon)
...
                            aggregator.update_address(addr)

集約した字幕は高度が更新されたタイミング、約1秒に1回出力します。

                    seg = aggregator.on_tick(t_rel)  # 高度は約1Hz
                    if seg:
                        self._write_segment(seg)


今回は1回だけREST APIへのリクエストして、データ名で処理を分岐しています。5

        for t_ns, _, data_name, data_bytes in self.reader.get_datapoints():

...
            if data_name == "1/pcm":
...
            elif data_name == "1/h264":
...
            elif data_name == "1/gnss_speed":
...
            elif data_name == "1/gnss_altitude":
...
            elif data_name == "1/gnss_coordinates":

MP4多重化

音声・映像・字幕それぞれで最初のデータポイントの相対時刻が異なるため、計測開始からのオフセット(ffmpegの-itsoffsetオプション)を与えて調整しています。

            cmd += [
...
                "-itsoffset",
                f"{opts.v_offset:.6f}",
                "-i",
                str(inputs.video),
...
            cmd += ["-itsoffset", f"{opts.a_offset:.6f}", "-i", str(inputs.audio)]
...
            cmd += ["-itsoffset", f"{opts.s_offset:.6f}", "-i", str(inputs.subtitle)]

おわりに

今回は、複数データを一般的な動画ファイルに多重化する例を紹介しました。

マルチモーダルデータも、SDKを使えば、自由に整形が可能です。

intdashはデータの時系列が管理されているため、出力時にもデータが同期されます。


なお、今回は海外eSIMを使ったiPhoneで計測を行いました。

海外での計測は以下の記事でも検証しています。

tech.aptpod.co.jp

リンク

本シリーズの過去記事はこちらからご覧ください。


  1. Media ExplorerでのMP4ダウンロードについては、SDK入門⑧で解説しています。
  2. サンプルプログラムをGitHubにて公開しています。Motionの計測データのみで動作確認しています。未回収データポイントがあると映像と音声がずれるなどの問題が起こりえます。
  3. サンプリングレートを48kHzにしています。元計測の音声データにあわせる必要があります。
  4. H.264のIDRフレームについては、SDK入門⑧で解説しています。
  5. 字幕集約がなければ、音声、映像ごとにリクエストする方がシンプルかもしれません。