from __future__ import annotations import json import os import shutil import subprocess import tempfile import unittest from pathlib import Path REPO = Path(__file__).resolve().parents[1] WORKSPACE = REPO / "video_workspace" FIXTURES = REPO / "tests" STATIC_FFMPEG_DIR = Path("/tmp/media_batch_video_tools/ffmpeg-7.0.2-amd64-static") AVISYNTH_LIB_DIR = Path("/tmp/media_batch_avisynth/install/lib") FFMS2_LIB_DIR = Path("/tmp/media_batch_ffms2_pkg/root/usr/lib/x86_64-linux-gnu") FFMS2_PLUGIN = Path("/tmp/media_batch_ffms2_avs_build_rc2/ffms2_avisynth_hidden.so") AVS_RUNNER = REPO / "tools" / "avisynth_runner" / "mbt_avs_runner" X264 = Path("/tmp/mbt-x264-build/x264") MKVMERGE = Path("/tmp/mbt-video-mux-tools/bin/mkvmerge") def scenario_environment() -> dict[str, str]: env = os.environ.copy() env["PATH"] = ( f"{STATIC_FFMPEG_DIR}:{X264.parent}:" f"{MKVMERGE.parent}:{env.get('PATH', '')}" ) env["LD_LIBRARY_PATH"] = ( f"{AVISYNTH_LIB_DIR}:{FFMS2_LIB_DIR}:" f"{env.get('LD_LIBRARY_PATH', '')}" ) env["MBT_FFMS2_PLUGIN"] = str(FFMS2_PLUGIN) env["MBT_AVS_RUNNER"] = str(AVS_RUNNER) env["MBT_VIDEO_FINAL_ACTION"] = "leave" env["MBT_VIDEO_TIMESTAMP_NAMES"] = "0" env["MBT_VIDEO_CODEC"] = "x264" env["MBT_VIDEO_CRF"] = "35" env["MBT_VIDEO_PRESET"] = "ultrafast" return env def require_scenario_prerequisites() -> dict[str, str]: missing: list[str] = [] if not AVS_RUNNER.is_file(): missing.append(str(AVS_RUNNER)) if not FFMS2_PLUGIN.is_file(): missing.append(str(FFMS2_PLUGIN)) if not X264.is_file(): missing.append(str(X264)) if not MKVMERGE.is_file(): missing.append(str(MKVMERGE)) if not STATIC_FFMPEG_DIR.is_dir(): missing.append(str(STATIC_FFMPEG_DIR)) if not AVISYNTH_LIB_DIR.is_dir(): missing.append(str(AVISYNTH_LIB_DIR)) if not FFMS2_LIB_DIR.is_dir(): missing.append(str(FFMS2_LIB_DIR)) for tool in ("ffmpeg", "ffprobe", "exiftool", "x264", "mkvmerge"): if shutil.which(tool, path=scenario_environment()["PATH"]) is None: missing.append(tool) if missing: raise unittest.SkipTest( "video end-to-end prerequisites are unavailable: " + ", ".join(missing) ) return scenario_environment() def ffprobe_json(path: Path, env: dict[str, str]) -> dict: completed = subprocess.run( [ "ffprobe", "-v", "error", "-show_streams", "-show_format", "-of", "json", str(path), ], check=True, capture_output=True, text=True, env=env, ) return json.loads(completed.stdout) def ffprobe_frame_timestamps(path: Path, env: dict[str, str]) -> list[float]: completed = subprocess.run( [ "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "frame=best_effort_timestamp_time", "-of", "json", str(path), ], check=True, capture_output=True, text=True, env=env, ) data = json.loads(completed.stdout) timestamps: list[float] = [] for frame in data.get("frames", []): value = frame.get("best_effort_timestamp_time") if value is not None: timestamps.append(float(value)) return timestamps def exiftool_json(path: Path, env: dict[str, str]) -> dict: completed = subprocess.run( ["exiftool", "-json", "-a", "-u", "-G0:1", str(path)], check=True, capture_output=True, text=True, env=env, ) return json.loads(completed.stdout)[0] class VideoEncodeEndToEndTests(unittest.TestCase): maxDiff = None def setUp(self) -> None: self.env = require_scenario_prerequisites() if WORKSPACE.exists(): shutil.rmtree(WORKSPACE) def tearDown(self) -> None: if WORKSPACE.exists(): shutil.rmtree(WORKSPACE) def copy_fixture(self, fixture_name: str, temp_root: Path) -> Path: source = FIXTURES / fixture_name if not source.is_file(): raise unittest.SkipTest(f"fixture missing: {source}") target = temp_root / fixture_name shutil.copy2(source, target) return target def create_workspace(self, source: Path) -> Path: self.run_video_encode(source, encode=False) script = WORKSPACE / f"{source.name}.avs" self.assertTrue(script.is_file(), script) return script def run_video_encode( self, source: Path, *, encode: bool, extra_env: dict[str, str] | None = None, ) -> str: env = self.env.copy() env["MBT_ENCODE_VIDEO"] = "1" if encode else "0" if extra_env: env.update(extra_env) completed = subprocess.run( ["python3", "video_encode.py", str(source)], cwd=REPO, check=True, capture_output=True, text=True, env=env, ) return completed.stdout def replace_user_script_body(self, script: Path, body: str) -> None: marker = "# User edits below this line. The imported source clip will already be in \"last\"." text = script.read_text(encoding="utf-8") self.assertIn(marker, text) prefix = text.split(marker, 1)[0] + marker + "\n\n" script.write_text(prefix + body.strip() + "\n", encoding="utf-8") def assert_video_stream_frames(self, output: Path, expected: int) -> None: data = ffprobe_json(output, self.env) video_stream = next( stream for stream in data["streams"] if stream["codec_type"] == "video" ) frame_count = int(video_stream.get("nb_frames") or len(ffprobe_frame_timestamps(output, self.env))) self.assertEqual(frame_count, expected) def assert_audio_codec(self, output: Path, expected: str) -> None: data = ffprobe_json(output, self.env) audio_stream = next( stream for stream in data["streams"] if stream["codec_type"] == "audio" ) self.assertEqual(audio_stream["codec_name"], expected) def test_real_frame_validation_mode_renders_the_source(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("20260602_222842.mp4", Path(temp_dir)) self.create_workspace(source) validation_script = WORKSPACE / ".source" / f"{source.name}.validate.avs" validation_csv = WORKSPACE / ".source" / f"{source.name}.real.frames.csv" completed = subprocess.run( [str(AVS_RUNNER), "--validate-real", str(validation_script), str(validation_csv)], check=True, capture_output=True, text=True, env=self.env, ) self.assertIn("rendered=170", completed.stdout) self.assertEqual(len(validation_csv.read_text(encoding="utf-8").splitlines()), 171) def test_cfr_mp4_no_edit_copies_metadata(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) self.create_workspace(source) self.run_video_encode( source, encode=True, extra_env={"MBT_VIDEO_CONTAINER": "mp4", "MBT_VIDEO_AUDIO": "aac"}, ) output = WORKSPACE / "C0011.MP4.mp4" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 60) self.assert_audio_codec(output, "aac") self.assertFalse(output.with_name(f"{output.name}.manifest.json").exists()) self.assertFalse((WORKSPACE / ".source" / "avisynth_helpers.avs").exists()) self.assertFalse(list((WORKSPACE / ".source").rglob("*.frames.csv"))) self.assertFalse(list((WORKSPACE / ".source").rglob("*.runner.log"))) def test_vfr_mp4_trim_preserves_location_and_adjusts_end_timestamp(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("20260602_222842.mp4", Path(temp_dir)) script = self.create_workspace(source) self.replace_user_script_body(script, "Trim(last, 0, 84)\nlast") self.run_video_encode( source, encode=True, extra_env={"MBT_VIDEO_CONTAINER": "mp4", "MBT_VIDEO_AUDIO": "aac"}, ) output = WORKSPACE / "20260602_222842.mp4.mp4" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 85) metadata = exiftool_json(output, self.env) self.assertIn("Composite:GPSLatitude", metadata) self.assertIn("Composite:GPSLongitude", metadata) self.assertEqual(metadata.get("QuickTime:CreateDate"), "2026:06:02 13:28:46") def test_vfr_mp4_x265_uses_post_encode_timecodes(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("20260602_222842.mp4", Path(temp_dir)) script = self.create_workspace(source) self.replace_user_script_body(script, "Trim(last, 0, 84)\nlast") self.run_video_encode( source, encode=True, extra_env={ "MBT_VIDEO_CONTAINER": "mp4", "MBT_VIDEO_CODEC": "x265", "MBT_VIDEO_AUDIO": "aac", }, ) output = WORKSPACE / "20260602_222842.mp4.mp4" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 85) def test_select_even_uses_segmented_aac_audio(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) script = self.create_workspace(source) self.replace_user_script_body(script, "SelectEven(last)\nlast") self.run_video_encode( source, encode=True, extra_env={"MBT_VIDEO_CONTAINER": "mp4", "MBT_VIDEO_AUDIO": "aac"}, ) output = WORKSPACE / "C0011.MP4.mp4" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 30) self.assert_audio_codec(output, "aac") def test_mixed_normal_delete_and_drop_frame_preserves_drop_durations(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) script = self.create_workspace(source) self.replace_user_script_body( script, """ SelectEvery(last, 4, 0, 2, 3) MBT_DropEvery(last, 3, 2) last """, ) self.run_video_encode( source, encode=True, extra_env={"MBT_VIDEO_CONTAINER": "mp4", "MBT_VIDEO_AUDIO": "aac"}, ) output = WORKSPACE / "C0011.MP4.mp4" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 30) def test_drop_frame_range_helper(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) script = self.create_workspace(source) self.replace_user_script_body(script, "MBT_Drop(last, 1, 2)\nlast") self.run_video_encode( source, encode=True, extra_env={"MBT_VIDEO_CONTAINER": "mp4", "MBT_VIDEO_AUDIO": "aac"}, ) output = WORKSPACE / "C0011.MP4.mp4" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 58) def test_rotate_crop_helper_crops_a_full_frame_turn(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-rotate-crop-") as temp_dir: temp = Path(temp_dir) helper = (REPO / "tools" / "avisynth_helpers.avs").as_posix() cases = [ ("source-dar", "RotateCrop(last, 10.0)", "width=1484,height=832"), ("square", "RotateCrop(last, 10.0, 1.0)", "width=932,height=932"), ] for name, call, expected in cases: script = temp / f"{name}.avs" csv = temp / f"{name}.csv" script.write_text( "\n".join( [ 'function Turn(clip c, float "angle", int "lx", int "rx", int "ty", int "by")', "{", ' Assert(!Defined(lx) && !Defined(rx) && !Defined(ty) && !Defined(by), "RotateCrop must not limit Turn\'s display rectangle")', " return c", "}", f'Import("{helper}")', 'BlankClip(width=1920, height=1080, pixel_type="YV12")', call, ] ) + "\n", encoding="utf-8", ) completed = subprocess.run( [str(AVS_RUNNER), "--validate", str(script), str(csv)], capture_output=True, text=True, env=self.env, ) self.assertEqual(completed.returncode, 0, completed.stderr) self.assertIn(expected, completed.stdout) def test_simple_trim_can_copy_audio(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) script = self.create_workspace(source) self.replace_user_script_body(script, "Trim(last, 10, 49)\nlast") self.run_video_encode( source, encode=True, extra_env={"MBT_VIDEO_CONTAINER": "mp4", "MBT_VIDEO_AUDIO": "copy"}, ) output = WORKSPACE / "C0011.MP4.mp4" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 40) self.assert_audio_codec(output, "pcm_s16be") def test_mkv_opus_output(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) self.create_workspace(source) self.run_video_encode( source, encode=True, extra_env={"MBT_VIDEO_CONTAINER": "mkv", "MBT_VIDEO_AUDIO": "opus"}, ) output = WORKSPACE / "C0011.MP4.mkv" self.assertTrue(output.is_file(), output) self.assert_video_stream_frames(output, 60) self.assert_audio_codec(output, "opus") def test_existing_valid_editable_script_is_kept(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) script = self.create_workspace(source) original_text = script.read_text(encoding="utf-8") output = self.run_video_encode(source, encode=False) self.assertIn("existing editable scripts", output) self.assertEqual(script.read_text(encoding="utf-8"), original_text) def test_existing_invalid_editable_script_is_rejected_noninteractive(self) -> None: with tempfile.TemporaryDirectory(prefix="mbt-video-e2e-") as temp_dir: source = self.copy_fixture("C0011.MP4", Path(temp_dir)) script = self.create_workspace(source) script.write_text( 'BlankClip(length=1, width=16, height=16, pixel_type="YV12")\n', encoding="utf-8", ) env = self.env.copy() env["MBT_ENCODE_VIDEO"] = "0" completed = subprocess.run( ["python3", "video_encode.py", str(source)], cwd=REPO, capture_output=True, text=True, env=env, ) self.assertNotEqual(completed.returncode, 0) self.assertIn("invalid kept script", completed.stdout) self.assertIn("not overwritten", completed.stdout) if __name__ == "__main__": unittest.main()