mcpField
R
rerun-mp4

rerun-mp4

github.com/rerun-ioデータ・データベース11.7k53 回インストール今日更新
リポジトリ

rerun-mp4 について

MP4ファイルを1つRerunに取り込むためのリーダースキルです。圧縮動画サンプルをGOP単位で扱う遅延チャンクストリームを生成し、VideoStreamで表現できないコーデックやクリップにはアセットモードを使えます。

主な機能

  1. 01単一MP4ファイルの遅延読み込み
  2. 02GOP単位の圧縮動画サンプル出力
  3. 03フォールバック用アセットモード
  4. 04出力コーデックとGOPサイズの設定
  5. 05動画タイムラインの名前変更と再タグ付け

活用シーン

  1. 01カメラのMP4映像をRerunに取り込む
  2. 02複数カメラの動画を1つの記録に統合
  3. 03センサー間の時刻合わせに動画時刻を再タグ付け
  4. 04VideoStreamに適さないコーデックやクリップの処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rerun-io/rerun --skill rerun-mp4

インストール後、Claude Code で/rerun-mp4を入力して読み込まれたか確認します。

詳細

Rerun の mp4 取り込み

Mp4Reader は、1 つの .mp4 ファイルを 1 つのエンティティ上の遅延チャンクストリームに変換します。 圧縮されたビデオサンプルを扱い、ピクセルへのデコードは行わず、必要な場合を除いて再エンコードもしません。設定はすべてコンストラクターで行い、stream() に引数はありません。 .stream() の後のストリーム処理(フィルター、マップ、マージ、収集、書き込み)については、rerun-chunk-processing を参照してください。

API

from rerun.experimental import Mp4Reader, Mp4TranscodeOptions

reader = Mp4Reader(video_path, entity_path="/camera/front")  # mode="stream" by default
stream = reader.stream()  # lazy: nothing is decoded yet

path より後のすべてのパラメーターはキーワード専用です。1 つのリーダーで扱えるファイルは 1 つです。複数のカメラを扱う場合は、ファイルごとにリーダーを作成し、LazyChunkStream.merge(...) でマージしてください(下記参照)。

2 つのモード

mode出力されるもの使用する場面
"stream"(デフォルト)コーデックチャンク、GOP ごとのサンプルチャンク、キーフレームマーカーほとんどの場合 — すべてのフレームに時間が割り当てられ、クエリ可能です
"asset"ファイル全体を 1 つの BLOB として、さらにフレームインデックスコーデックを VideoStream にできない場合、またはソースが GOP の途中から始まる場合

通常はストリームモードを使用してください。次のセクションで、出力される内容を詳しく説明します。アセットモードではファイル全体を 1 つの BLOB として記録にコピーするため、後続処理でアセット全体を使わずに単一のフレームを確認することはできません。VideoStream で扱えないコーデック(mp4v、画像シーケンスの mp4)に対するフォールバックであり、rerun video.mp4 と組み込みのファイルインポーターもこのモードを使用します。

ストリームモードの出力内容

tests/assets/video/Big_Buck_Bunny_1080_1s_h264_nobframes.mp4(30 フレーム、1 GOP)を entity_path="/camera/front" で読み込む場合、出力全体は 3 つのチャンクになります。

[0] entity=/camera/front static=True rows=1 timelines=[] cols=['VideoStream:codec']
[1] entity=/camera/front static=False rows=30 timelines=['video'] cols=['VideoStream:sample']
[2] entity=/camera/front static=False rows=1 timelines=['video'] cols=['VideoStream:is_keyframe']
  • 1 行を含む静的チャンクが 1 つで、VideoStream:codec を保持します。 タイムラインはありません。ストリームを走査するコードでは、これに対応する必要があります(if chunk.is_static: continue)。
  • GOP ごとに時間チャンクが 1 つです。キーフレームと、それに依存するすべてのサンプル(次のキーフレームは含みません)が入ります。ここに入るのはサンプルのみで、キーフレームフラグは列に含まれません。
  • 末尾に is_keyframe マーカーチャンクが 1 つあり、各キーフレームの時刻にスパースな True の行を保持し、それ以外は何も保持しません。マーカーをサンプルチャンクから分離することで、キーフレームのみのクエリでサンプルのペイロードをスキップでき、collect(optimize=…) が正規形式として受け入れられるようになります。行ごとのリストなので、読み戻した行は True ではなく [True] になります。
  • タイムラインは video、duration[ns] の値は動画の開始からの mp4 PTS です。
  • すべてのチャンクが同じエンティティ上にあります。 entity_path=None(デフォルト)の場合、ファイルの絶対パスから導出されるため、/data から実行した foo/video.mp4 は /data/foo/video.mp4 になります。実際のパイプラインでは entity_path を明示的に指定してください。

短いクリップは 1 つの GOP であることが多いため、「合計 3 チャンク」は一般的な形であり、何かが欠落したという意味ではありません。chunk_by_gop=False にすると、代わりにサンプルごとに 1 つの Rerun チャンクが出力されます(マーカーチャンクには影響しません)。これはデバッグ用の形式です。フレームごとにチャンクを分けるのはストレージレイアウトとして適切ではなく、後続の最適化処理も 6× 高コストになります(以下で測定)。個々のサンプルを調べる場合を除き、デフォルトのままにしてください。

位置や is_static ではなく、列によって 3 種類を区別してください。コーデックチャンクは静的で、サンプルチャンクには VideoStream:sample が含まれ、マーカーチャンクには VideoStream:is_keyframe のみが含まれます。

ストリームモードでサポートされるコーデックは、VideoCodec の次の 5 つの値のみです。H264、H265、AV1、VP8、VP9。

FFmpeg によるトランスコード

VideoStream はまだ DTS != PTS をモデル化できないため、コンテナレベルで B フレームの並べ替えが行われる H.264/H.265 ソースは、直接出力できません。リーダーがこれを独自に処理します。-bf 0 を指定して FFmpeg で再エンコードし、結果をフラグメント化された mp4 としてストリーミングで返し、各フラグメントを 1 つの GOP チャンクに変換します。そのため、一度にメモリ上に保持される GOP は 1 つだけです。これは自動的に行われ、API からは見えません。出力形式はクリーンなソースと同じです。ffmpeg 実行ファイルが必要です。アセットモードは B フレームの影響を受けません。

Mp4TranscodeOptions(ストリームモードのみ)では、さらにトランスコードを 要求 できます。

フィールド効果
output_codec別の VideoCodec に再エンコードします。出力される VideoStream:codec に反映されます
gop_sizeN フレームごとにキーフレームを強制します。ビューアーでのシークコストを調整する設定です
try_gpuハードウェアエンコードをベストエフォートで使用します。NVENC / VideoToolbox のみです
ffmpeg_overridePATH 上のものではなく、この ffmpeg を使用します

ソースですでに使われている output_codec を要求しても、何も行われません。FFmpeg を使わない直接処理のままです。chunk_by_gop=True の場合、gop_size=N を指定すると、最後以外のすべての GOP チャンクがちょうど N サンプルを保持します。たとえば、30 フレームのクリップで gop_size=10 を指定すると、10、10、10 になります。try_gpu が現実的に対応するのは H264/H265 と、新しい NVIDIA GPU での AV1 です。VP8/VP9 は常にソフトウェア処理にフォールバックします。また、トランスコードがすでに行われている場合にのみ機能します。

Mp4Reader(
    video_path,
    entity_path="/camera/front",
    # ~1s GOPs on a 60fps source, so seeking in the viewer stays snappy.
    transcode=Mp4TranscodeOptions(gop_size=64),
).stream()

タイムライン:mp4 の PTS は録画の時計ではありません

出力される時刻は常に PTS です。つまり、その動画 の開始からの経過ナノ秒です。複数のセンサーを使った録画では、これを基準に位置合わせしたいことはほとんどありません。

  • timeline_name="real_time" はタイムラインの名前を変更します。
  • timeline_type="timestamp" は、同じ PTS 値を Unix エポックからのナノ秒として型付けし直すだけです。リーダーは時刻をシフトしないため、それ単独では動画が 1970 年付近に表示されます。タグを付け直す手順と組み合わせた場合にのみ意味があります。
  • stream.map(...) でタグを付け直します。 サンプルは表示順に到着します(B フレームはすでに除去済みです)。そのため、進行中のカーソルを使ってサンプル i を capture_times_ns[i] に対応付けます。ただし、サンプルを含まないキーフレームマーカーチャンクはカーソルでスキップする必要があります。そのチャンクは、サンプルにすでに割り当てられている PTS を使って対応付けてください。
SAMPLE_COL = "VideoStream:sample"


def _reindex_to_capture_times(stream, capture_times_ns, timeline_name):
    cursor = 0
    pts_to_time = {}

    def _retag(chunk):
        nonlocal cursor
        if chunk.is_static:  # the codec chunk carries no timeline
            return chunk
        batch = chunk.to_record_batch()
        col_index = batch.schema.get_field_index(timeline_name)
        old_field = batch.schema.field(col_index)
        old_pts = np.asarray(batch.column(col_index).cast(pa.int64()))

        if SAMPLE_COL in batch.schema.names:
            # Clamp in case the decoder yields a slightly different frame count.
            indices = np.clip(np.arange(cursor, cursor + chunk.num_rows), 0, len(capture_times_ns) - 1)
            cursor += chunk.num_rows
            new_times_ns = capture_times_ns[indices]
            pts_to_time.update(zip(old_pts.tolist(), new_times_ns.tolist()))
        else:
            # The sparse keyframe marker, emitted after every sample chunk.
            new_times_ns = np.array([pts_to_time[pts] for pts in old_pts.tolist()], dtype=np.int64)

        times = pa.array(new_times_ns.astype("datetime64[ns]"))
        # `metadata=` is load-bearing — see gotcha 3.
        new_field = pa.field(old_field.name, times.type, nullable=old_field.nullable, metadata=old_field.metadata)
        return Chunk.from_record_batch(batch.set_column(col_index, new_field, times))[0]

    return stream.map(_retag)

このカーソル処理は順序に依存するため、タグの付け直しは、マージする前に単一リーダーのストリーム上で行ってください。これは DROID ローダーのパターンです。参考資料を参照してください。

「すべての非静的チャンクは順番に並んだサンプルである」と仮定する map / flat_map には、同じ問題があります。マーカーチャンクを進めてしまい、次に来る時刻をそのチャンクに割り当てるため、キーフレームマーカーが対応するサンプルから知らないうちにずれてしまいます。

複数のカメラを 1 つの録画にまとめる

ファイルごとにリーダーを 1 つ用意し、エンティティパスをそれぞれ別にしてからマージします。

streams = [
    Mp4Reader(path, entity_path=f"/camera/{name}", timeline_name="real_time", timeline_type="timestamp").stream()
    for name, path in cameras.items()
]
(
    LazyChunkStream
    .merge(*streams)
    .collect(optimize=OptimizationProfile.OBJECT_STORE)
    .write_rrd(out_path, application_id=app_id, recording_id=segment_id)
)

OBJECT_STORE は動画を GOP 単位で再バッチ処理し、リーダーのキーフレームマーカーをそのまま保持します。そのため、fix_keyframe=True は必要ありません。skipping GoP rebatching … is_keyframe data is incorrect が表示される場合は、上流の処理でマーカーが書き換えられています(落とし穴 4 を参照)。その場合、fix_keyframe=True はエンコード済みサンプルからマーカーを再導出するための回避策になります。

reader のチャンクと optimize のチャンクの関係

reader と collect(optimize=…) はGOP の開始位置については一致しますが、1 つのチャンクを共有する GOP の数については一致しません。これは意図的な設計です。

  • reader はGOP に合わせた最も細かい分割を出力します。GOP ごとにチャンクが 1 つずつです。データがどのプロファイル向けかは reader には分からないため、これ以上細かくすることはできません。
  • その後 optimize がサイズポリシーを適用し、プロファイルの max_bytes / max_rows に達するまで連続する GOP を結合します。GOP を複数のチャンクに分割することはないため、維持される境界はすべて reader がすでに出力したものです。

したがって、optimize の分割は reader の分割を単に粗くしたものです。サンプルの順序は同じで、最適化後の各チャンクは reader の GOP 全体が連続したものであり、キーフレームのマーカーも変更されません。各 GOP がすでにプロファイルの予算に近い場合は、両者の結果は同じになります(gop_size=10 の 30 フレームのクリップなら、どちらも 3 チャンクです)。GOP が小さい場合は大きく異なり、12 GOP のクリップは OBJECT_STORE では 1 チャンク、LIVE では 4 チャンクになります。

実用上の結論は、optimize の「助け」になるよう reader の出力を事前に結合したり、再チャンクしたりしないことです。GOP ごとのストリームをそのまま渡し、プロファイルに判断させてください。また、optimize を完全に省略して send_chunks に直接書き込む場合(DROID ローダーのように)は、最も細かい分割のまま保存されます。これは正しい動作ですが、オブジェクトストレージが望むよりチャンク数が多くなります。

GOP 単位に分割された入力なら optimize の処理は軽量です

reader の出力に対して optimize を実行するのは、避ける価値があるほど無駄な処理ではありません。 89 MB / 1800 フレーム / 60 GOP の H.264 ファイルの場合:

collect()collect(optimize=OBJECT_STORE)差分
chunk_by_gop=True(デフォルト)11.1 ms16.3 ms+5.2 ms(1.5×)
chunk_by_gop=False14.2 ms48.6 ms+34.4 ms(3.4×)

GOP に揃えた入力では、処理のどちらの部分も高コストではありません。

  • GOP の開始位置の検出はヘッダーだけで行われます。 build_sample_index は各サンプルに対して detect_gop_start を実行しますが、読み取るのは各サンプルのヘッダーの数バイトだけです。デコードは行いません。上記の optimize 全体の差分(5.2 ms)は、同じ 1800 サンプルに対して Python ループで同じ検出処理を呼び出す時間(6.0 ms)より短く、その大半は FFI のオーバーヘッドです。
  • チャンクの再構築では何もコピーされません。 chunk_from_gop は taken(0..n) を呼び出し、re_arrow_util::take_array は、インデックスが配列全体にわたってゼロから連続している場合、配列をコピーせずに返します。GOP ごとにチャンクが 1 つであれば、常にこの条件を満たすため、数 MB のサンプルバッファが複製されることはありません。つまり、89 MB の「再構築」が 5 ms で済みます。

この高速パスを利用できないのが chunk_by_gop=False の場合です。この場合、各 GOP はソースチャンク 30 個にまたがるため、chunk_from_gop はそれらを concat_and_sort する必要があり、サンプルのバイト列も実際にコピーされます。また、同じファイルで LIVE は OBJECT_STORE よりも処理コストが高くなります(+13.2 ms 対 +5.2 ms)。チャンクの予算が大幅に小さいため、完了できないコンパクションを試みるからです。GOP が分割されることはありません。

各カメラのリタグは、マージする前に行ってください。上記のリタグ処理はカーソルを使ってチャンクをたどるため、1 つの動画のチャンクが順番どおりに並んでいる必要があります。

注意点

  1. エラーは構築時ではなく、最初の読み取り時に発生します。 コンストラクターで確認するのは、ファイルの存在と引数の検証のみです。stream() は遅延パイプラインを構築するため、こちらも成功します。コーデックのサポート、キーフレームの配置、FFmpeg の有無はストリームの実行時に確認されるため、Mp4Reader(…) の呼び出しではなく、消費処理(to_chunks()、send_chunks、反復処理)をラップしてください。
  2. 5 つの VideoCodec 値に含まれないコーデックは VideoStream にできません。 mp4v(MPEG-4 Part 2)は、ロボットのデータセットで実際に遭遇するコーデックです。これを使うと RuntimeError: MP4 error: MP4 demux: Video track uses unsupported codec "mp4v" が発生します。アセットモードではファイルを受け入れますが、一部の処理に限られます。フレームのタイムスタンプも読み取れないため、blob チャンクは出力されますが、VideoFrameReference のインデックスチャンクは警告とともにスキップされます。つまり、バイト列は得られてもタイムラインは得られません。そのカメラをスキップして、記録プロパティにその事実を記録する(DROID ローダーの方式)のは、通常、タイムラインのない blob より適切です。どちらを選んでも、カメラ 1 台のためにエピソード全体の処理を失敗させるよりはましです。
  3. 時間列を書き換える map/flat_map では、フィールドメタデータも引き継ぐ必要があります。 metadata=old_field.metadata を指定せずに pa.field(...) を使うと、rerun:kind: 'index' が失われ、再構築されたチャンクはエラーもタイムラインもなく、暗黙に静的になります。その結果、サンプルは時間軸にまったく配置されません。
  4. 末尾のマーカーチャンクは時間情報を持ちますが、サンプルは含みません。 そのため、not chunk.is_static を条件とするチャンク単位の処理では、これもサンプルとして処理されます。代わりに VideoStream:sample 列を条件にしてください。上記のリタグ付けを参照してください。
  5. アセットモードは Arrow の i32 オフセットにより約 2 GiB が上限で、ファイルのバイト列を RRD に複製します。
  6. mode="asset" では、chunk_by_gop=False と transcode= の両方が ValueError になります。これらの設定はストリームモード専用です。
  7. timeline_type="timestamp" を指定するだけでは、何もシフトされません。リタグ付けをしないと、動画はエポックに配置されます。
  8. リーダーが出力するのは圧縮サンプルであり、ピクセルではありません。 サムネイル、CLIP 埋め込み、その他 RGB が必要なものは、ファイルを別途デコードする必要があります(OpenCV、PyAV)。VideoStream のチャンクからは取得できません。
  9. stream() を呼び出すたびにファイルが最初から再デコードされ、終端メソッドを呼び出すたびにパイプラインが再実行されます。複数回の処理が必要な場合は、一度だけ collect() を呼び出してください。
  10. 静的なコーデックチャンクは、どの map/flat_map でも明示的に処理してください。これはタイムラインもサンプルも持たないため、時間列をそのままインデックス参照すると失敗します。
  11. 最初のキーフレームより前にあるサンプルは、ストリームモードでは拒否されます(デコーダーは GOP の途中から開始できません)。このようなファイルにはアセットモードを使用してください。

参考資料

  • 一連の処理を網羅した標準的な例:rerun_py/tests/integration/test_mp4_reader.py(両方のモード、chunk_by_gop、エンティティパス、timeline_type、トランスコード変換、エラーケース)。
  • Rust コア:crates/data_flow/re_mp4_reader/(GOP/トランスコードの処理は stream.rs、blob とインデックスの処理は asset.rs)。crates/data_flow/re_mp4_reader/tests/stream.rs ではコーデックの組み合わせと GOP の間隔を確認しています。
  • rerun-chunk-processing(ストリーム/レンズの仕組み)、rerun-data-model(記録内で動画、キャリブレーション、サムネイルを配置する場所)。

コメント