Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/lerobot/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ def _transcode_mp4_to_h264(mp4_path: Path, gop_seconds: float, fps: float) -> li
"-sc_threshold",
"0",
"-x264-params",
"aud=1",
# bframes=0: B-frame streams lose their reorder-buffer tail through
# the raw Annex B -> MP4 remux, undercounting decoded_frame_count (#250).
"aud=1:bframes=0",
"-f",
"h264",
"pipe:1",
Expand Down
85 changes: 85 additions & 0 deletions tests/test_lerobot_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import importlib.util
import json
import os
import shutil
import subprocess
from pathlib import Path

import pytest
Expand All @@ -19,6 +22,16 @@
_DERIVE = prep._derive_numeric_schema
_ENCODE = prep._encode_cdr_float32_array

_FFMPEG = shutil.which("ffmpeg")
_FFPROBE = shutil.which("ffprobe")

# Only the remux test below shells out. Scoping this to the module would skip
# the five metadata tests too, and none of those touch ffmpeg at all.
_requires_system_ffmpeg = pytest.mark.skipif(
_FFMPEG is None or _FFPROBE is None,
reason="system ffmpeg/ffprobe required: prepare.py shells out to bare 'ffmpeg'",
)


def _build_fake_corpus(tmp_path: Path) -> dict:
"""Synthetic v3 metadata: 4 episodes, 2 cameras, 6-dim state/action."""
Expand Down Expand Up @@ -245,3 +258,75 @@ def fake_dl(url: str, dest: Path, **kw: object) -> None:
output_dir=tmp_path / "out",
camera_key="observation.nope",
)


@_requires_system_ffmpeg
def test_converter_output_remuxes_without_tail_loss(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The converter's stream must decode in full through the hflow remux.

B-frame H.264 loses its reorder-buffer tail when remuxed from raw Annex
B to MP4, so decoded_frame_count undercounts healthy episodes (#250).
The converter encodes with bframes=0; prove the full chain decodes
every source frame.
"""

assert _FFMPEG is not None and _FFPROBE is not None
# prepare.py shells out to bare "ffmpeg"/"ffprobe"; make sure it
# resolves to the system binary this test found.
monkeypatch.setenv(
"PATH",
str(Path(_FFMPEG).resolve().parent) + os.pathsep + os.environ.get("PATH", ""),
)

source = tmp_path / "source.mp4"
subprocess.run(
[
_FFMPEG,
"-hide_banner",
"-y",
"-f",
"lavfi",
"-i",
"testsrc2=size=160x120:rate=30:duration=3,format=yuv420p",
"-c:v",
"libx264",
"-preset",
"medium",
"-g",
"30",
"-keyint_min",
"30",
"-sc_threshold",
"0",
str(source),
],
capture_output=True,
check=True,
)

from hflow.video import write_access_units_to_mp4

units = prep._transcode_mp4_to_h264(source, gop_seconds=1.0, fps=30.0)
muxed = write_access_units_to_mp4(units, fps=30.0, output=tmp_path / "remux.mp4")

probe = subprocess.run(
[
_FFPROBE,
"-v",
"error",
"-count_frames",
"-select_streams",
"v:0",
"-show_entries",
"stream=nb_read_frames",
"-of",
"default=noprint_wrappers=1",
str(muxed),
],
capture_output=True,
text=True,
check=True,
)
assert "nb_read_frames=90" in probe.stdout, probe.stdout
Loading