Files
playground/ffmpeg_remove_still_test.py

108 lines
5.1 KiB
Python
Executable File

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["pytest"]
# ///
import json
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
import ffmpeg_remove_still as script
@pytest.mark.parametrize("log, expected", [
("", []),
("lavfi.freezedetect.freeze_start: 0", [(0.0, 10.0)]),
("lavfi.freezedetect.freeze_start: 2\nlavfi.freezedetect.freeze_end: 5", [(2.0, 5.0)]),
("lavfi.freezedetect.freeze_start: 2\nlavfi.freezedetect.freeze_end: 5\n"
"lavfi.freezedetect.freeze_start: 8", [(2.0, 5.0), (8.0, 10.0)]),
])
def test_parse_freezes(log: str, expected: list[tuple[float, float]]) -> None:
assert script.parse_freezes(log=log, duration=10.0) == expected
@pytest.mark.parametrize("freezes, keep_still, keyframes, expected", [
([], 1.0, [0.0], [(0.0, 10.0)]),
([(0.0, 10.0)], 1.0, [0.0, 0.5, 9.5], [(0.0, 0.5), (9.5, 10.0)]),
([(2.0, 6.0)], 2.0, [0.0, 3.0, 5.0], [(0.0, 3.0), (5.0, 10.0)]),
([(2.0, 3.0)], 2.0, [0.0, 2.5], [(0.0, 10.0)]),
([(0.0, 4.0), (6.0, 10.0)], 1.0, [0.0, 0.5, 3.5, 6.5, 9.5],
[(0.0, 0.5), (3.5, 6.5), (9.5, 10.0)]),
([(2.0, 6.0)], 1.0, [0.0, 3.0, 5.0, 7.0], [(0.0, 3.0), (5.0, 10.0)]),
([(2.0, 6.0)], 1.0, [0.0, 4.0, 8.0], [(0.0, 10.0)]),
([(2.0, 6.0)], 1.0, [0.0, 8.0], [(0.0, 10.0)]),
])
def test_plan_segments(freezes: list[tuple[float, float]], keep_still: float,
keyframes: list[float], expected: list[tuple[float, float]]) -> None:
assert script.plan_segments(duration=10.0, freezes=freezes, keep_still=keep_still, keyframes=keyframes) == expected
@pytest.mark.skipif(not shutil.which(cmd="ffmpeg") or not shutil.which(cmd="ffprobe"), reason="Requires FFmpeg")
@pytest.mark.parametrize("has_audio, keep_still, keyframe_interval, expected_duration", [
(False, 0.5, 5, 4.0), (True, 0.5, 5, 4.0), (True, 1.5, 5, 6.0),
(True, 0.5, 100, 8.0),
])
def test_copies_packets_without_encoding(tmp_path: Path, has_audio: bool, keep_still: float,
keyframe_interval: int, expected_duration: float) -> None:
source = tmp_path / "demo's recording.mp4"
output = tmp_path / "short.mp4"
command = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-nostdin"]
for pattern in ["testsrc2=size=64x64:rate=10:duration=1", "color=red:size=64x64:rate=10:duration=3",
"testsrc2=size=64x64:rate=10:duration=1", "color=blue:size=64x64:rate=10:duration=3"]:
command.extend(["-f", "lavfi", "-i", pattern])
if has_audio:
command.extend(["-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000:duration=8"])
command.extend(["-filter_complex", "[0:v][1:v][2:v][3:v]concat=n=4:v=1:a=0[v]", "-map", "[v]"])
if has_audio:
command.extend(["-map", "4:a", "-c:a", "aac"])
command.extend(["-c:v", "libx264", "-g", str(keyframe_interval), "-keyint_min", str(keyframe_interval),
"-sc_threshold", "0", "-bf", "2", "-flags", "+cgop", str(source)])
subprocess.run(args=command, check=True, capture_output=True)
copied = subprocess.run(
args=[sys.executable, str(Path(script.__file__).resolve()), str(source), "--output", str(output),
"--min-still", "2", "--keep-still", str(keep_still)],
check=True, capture_output=True, text=True,
)
assert "non-monoton" not in copied.stderr.lower()
result = subprocess.run(
args=["ffprobe", "-v", "error", "-show_streams", "-show_format", "-of", "json", str(output)],
check=True, capture_output=True, text=True,
)
metadata = json.loads(result.stdout)
video = next(stream for stream in metadata["streams"] if stream["codec_type"] == "video")
assert float(video["duration"]) == pytest.approx(expected_duration, abs=0.15)
audio = [stream for stream in metadata["streams"] if stream["codec_type"] == "audio"]
assert bool(audio) == has_audio
if has_audio:
assert float(audio[0]["duration"]) == pytest.approx(float(video["duration"]), abs=0.1)
packet_hashes = []
for path in (source, output):
packets = subprocess.run(
args=["ffprobe", "-v", "error", "-show_packets", "-show_data_hash", "sha256",
"-show_entries", "packet=stream_index,data_hash", "-of", "json", str(path)],
check=True, capture_output=True, text=True,
)
packet_hashes.append({(packet["stream_index"], packet["data_hash"])
for packet in json.loads(packets.stdout)["packets"]})
assert packet_hashes[1] <= packet_hashes[0]
decoded = subprocess.run(
args=["ffmpeg", "-v", "error", "-i", str(output), "-f", "null", "-"],
check=True, capture_output=True, text=True,
)
assert decoded.stderr == ""
original = source.read_bytes()
refused = subprocess.run(
args=[sys.executable, str(Path(script.__file__).resolve()), str(source), "--output", str(source), "--overwrite"],
capture_output=True, text=True,
)
assert refused.returncode == 1
assert source.read_bytes() == original
if __name__ == "__main__":
sys.exit(pytest.main(args=[str(Path(__file__).resolve()), *sys.argv[1:]]))