diff --git a/docs/module-internals.md b/docs/module-internals.md index 3f82478..4f8019c 100644 --- a/docs/module-internals.md +++ b/docs/module-internals.md @@ -149,6 +149,11 @@ regeneration. It centralizes container codecs, optional audio stream copying, metadata/chapter policy, encode-failure reporting, and atomic same-directory publication. Each mapped stream is allowed to reach its own end, so a copied audio tail is not shortened to the frame-input duration. +Both the raw-BGR and timestamped-NUT stdin inputs disable ffmpeg probing before +`pipe:0`: their format is already explicit, and ffmpeg 6 on Linux can otherwise +wait for its normal analysis window while the producer blocks on a full pipe +before the source-audio input opens. Command-order regressions cover both stdin +modes; the real Linux full-clip CI job guards process completion. `probe_video_encode_profile` reads the first source video stream with ffprobe and preserves the supported properties that survive the 8-bit BGR boundary: `yuv420p`/`yuv422p`/`yuv444p` chroma sampling, recognized color tags, encoder diff --git a/src/remove_ai_watermarks/video_encoding.py b/src/remove_ai_watermarks/video_encoding.py index c15931f..1f129d3 100644 --- a/src/remove_ai_watermarks/video_encoding.py +++ b/src/remove_ai_watermarks/video_encoding.py @@ -286,8 +286,12 @@ def raw_video_command( ffmpeg = shutil.which("ffmpeg") if ffmpeg is None: raise RuntimeError("Video processing requires ffmpeg on PATH") + # The pipe format and stream geometry are already explicit. FFmpeg 6 can + # otherwise wait for its normal analysis window while the producer blocks + # on a full pipe, before the second (audio) input has been opened. + pipe_input = ["-analyzeduration", "0", "-probesize", "32", "-i", "pipe:0"] frame_input = ( - ["-f", "nut", "-i", "pipe:0"] + ["-f", "nut", *pipe_input] if timestamped_input else [ "-f", @@ -298,8 +302,7 @@ def raw_video_command( f"{width}x{height}", "-r", f"{fps:.12g}", - "-i", - "pipe:0", + *pipe_input, ] ) command = [ diff --git a/tests/test_video.py b/tests/test_video.py index 23a8fb3..6df876f 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -1950,6 +1950,34 @@ class TestVideoVisibleScan: class TestVideoVisibleEncoding: + def test_raw_pipe_input_disables_redundant_ffmpeg_probing( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ): + from remove_ai_watermarks import video_encoding + + monkeypatch.setattr(video_encoding.shutil, "which", lambda _name: "/usr/bin/ffmpeg") + command = video_encoding.raw_video_command( + tmp_path / "source.mp4", + tmp_path / "clean.mp4", + width=12, + height=8, + fps=24.0, + strip_metadata=True, + crf=14, + profile=video_encoding.VideoEncodeProfile(), + ) + + pipe_position = command.index("pipe:0") + assert command[pipe_position - 5 : pipe_position] == [ + "-analyzeduration", + "0", + "-probesize", + "32", + "-i", + ] + @staticmethod def _patch_single_frame_encode( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_video_invisible.py b/tests/test_video_invisible.py index c0b217d..61ddaf3 100644 --- a/tests/test_video_invisible.py +++ b/tests/test_video_invisible.py @@ -102,7 +102,16 @@ def test_timestamped_encoder_reads_nut_and_passes_pts_through( ) assert "-copyts" in command - assert command[command.index("-f") : command.index("-f") + 4] == ["-f", "nut", "-i", "pipe:0"] + assert command[command.index("-f") : command.index("-f") + 8] == [ + "-f", + "nut", + "-analyzeduration", + "0", + "-probesize", + "32", + "-i", + "pipe:0", + ] assert command[command.index("-fps_mode") + 1] == "passthrough" assert command[command.index("-avoid_negative_ts") + 1] == "disabled" assert command[command.index("-enc_time_base:v") + 1] == "1/90000"