147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
"""
|
|
Tests for FFmpeg filter compilation.
|
|
|
|
Validates that each filter mapping produces valid FFmpeg commands.
|
|
"""
|
|
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from .ffmpeg_compiler import FFmpegCompiler, EFFECT_MAPPINGS
|
|
|
|
|
|
def test_filter_syntax(filter_str: str, duration: float = 0.1, is_complex: bool = False) -> tuple[bool, str]:
|
|
"""
|
|
Test if an FFmpeg filter string is valid by running it on a test pattern.
|
|
|
|
Args:
|
|
filter_str: The filter string to test
|
|
duration: Duration of test video
|
|
is_complex: If True, use -filter_complex instead of -vf
|
|
|
|
Returns (success, error_message)
|
|
"""
|
|
with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as f:
|
|
output_path = f.name
|
|
|
|
try:
|
|
if is_complex:
|
|
# Complex filter graph needs -filter_complex and explicit output mapping
|
|
cmd = [
|
|
'ffmpeg', '-y',
|
|
'-f', 'lavfi', '-i', f'testsrc=duration={duration}:size=64x64:rate=10',
|
|
'-f', 'lavfi', '-i', f'sine=frequency=440:duration={duration}',
|
|
'-filter_complex', f'[0:v]{filter_str}[out]',
|
|
'-map', '[out]', '-map', '1:a',
|
|
'-c:v', 'libx264', '-preset', 'ultrafast',
|
|
'-c:a', 'aac',
|
|
'-t', str(duration),
|
|
output_path
|
|
]
|
|
else:
|
|
# Simple filter uses -vf
|
|
cmd = [
|
|
'ffmpeg', '-y',
|
|
'-f', 'lavfi', '-i', f'testsrc=duration={duration}:size=64x64:rate=10',
|
|
'-f', 'lavfi', '-i', f'sine=frequency=440:duration={duration}',
|
|
'-vf', filter_str,
|
|
'-c:v', 'libx264', '-preset', 'ultrafast',
|
|
'-c:a', 'aac',
|
|
'-t', str(duration),
|
|
output_path
|
|
]
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
|
|
if result.returncode == 0:
|
|
return True, ""
|
|
else:
|
|
# Extract relevant error
|
|
stderr = result.stderr
|
|
for line in stderr.split('\n'):
|
|
if 'Error' in line or 'error' in line or 'Invalid' in line:
|
|
return False, line.strip()
|
|
return False, stderr[-500:] if len(stderr) > 500 else stderr
|
|
except subprocess.TimeoutExpired:
|
|
return False, "Timeout"
|
|
except Exception as e:
|
|
return False, str(e)
|
|
finally:
|
|
Path(output_path).unlink(missing_ok=True)
|
|
|
|
|
|
def run_all_tests():
|
|
"""Test all effect mappings."""
|
|
compiler = FFmpegCompiler()
|
|
results = []
|
|
|
|
for effect_name, mapping in EFFECT_MAPPINGS.items():
|
|
filter_name = mapping.get("filter")
|
|
|
|
# Skip effects with no FFmpeg equivalent (external tools or python primitives)
|
|
if filter_name is None:
|
|
reason = "No FFmpeg equivalent"
|
|
if mapping.get("external_tool"):
|
|
reason = f"External tool: {mapping['external_tool']}"
|
|
elif mapping.get("python_primitive"):
|
|
reason = f"Python primitive: {mapping['python_primitive']}"
|
|
results.append((effect_name, "SKIP", reason))
|
|
continue
|
|
|
|
# Check if complex filter
|
|
is_complex = mapping.get("complex", False)
|
|
|
|
# Build filter string
|
|
if "static" in mapping:
|
|
filter_str = f"{filter_name}={mapping['static']}"
|
|
else:
|
|
filter_str = filter_name
|
|
|
|
# Test it
|
|
success, error = test_filter_syntax(filter_str, is_complex=is_complex)
|
|
|
|
if success:
|
|
results.append((effect_name, "PASS", filter_str))
|
|
else:
|
|
results.append((effect_name, "FAIL", f"{filter_str} -> {error}"))
|
|
|
|
return results
|
|
|
|
|
|
def print_results(results):
|
|
"""Print test results."""
|
|
passed = sum(1 for _, status, _ in results if status == "PASS")
|
|
failed = sum(1 for _, status, _ in results if status == "FAIL")
|
|
skipped = sum(1 for _, status, _ in results if status == "SKIP")
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"FFmpeg Filter Tests: {passed} passed, {failed} failed, {skipped} skipped")
|
|
print(f"{'='*60}\n")
|
|
|
|
# Print failures first
|
|
if failed > 0:
|
|
print("FAILURES:")
|
|
for name, status, msg in results:
|
|
if status == "FAIL":
|
|
print(f" {name}: {msg}")
|
|
print()
|
|
|
|
# Print passes
|
|
print("PASSED:")
|
|
for name, status, msg in results:
|
|
if status == "PASS":
|
|
print(f" {name}: {msg}")
|
|
|
|
# Print skips
|
|
if skipped > 0:
|
|
print("\nSKIPPED (Python fallback):")
|
|
for name, status, msg in results:
|
|
if status == "SKIP":
|
|
print(f" {name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
results = run_all_tests()
|
|
print_results(results)
|