video-processor/tests/unit/test_advanced_encoders.py
Ryan Malloy bcd37ba55f Implement comprehensive 360° video processing system (Phase 4)
This milestone completes the video processor with full 360° video support:

## New Features
- Complete 360° video analysis and processing pipeline
- Multi-projection support (equirectangular, cubemap, EAC, stereographic, fisheye)
- Viewport extraction and animated viewport tracking
- Spatial audio processing (ambisonic, binaural, object-based)
- 360° adaptive streaming with tiled encoding
- AI-enhanced 360° content analysis integration
- Comprehensive test infrastructure with synthetic video generation

## Core Components
- Video360Processor: Complete 360° analysis and processing
- ProjectionConverter: Batch conversion between projections
- SpatialAudioProcessor: Advanced spatial audio handling
- Video360StreamProcessor: Viewport-adaptive streaming
- Comprehensive data models and validation

## Test Infrastructure
- 360° video downloader with curated test sources
- Synthetic 360° video generator for CI/CD
- Integration tests covering full processing pipeline
- Performance benchmarks for parallel processing

## Documentation & Examples
- Complete 360° processing examples and workflows
- Comprehensive development summary documentation
- Integration guides for all four processing phases

This completes the roadmap: AI analysis, advanced codecs, streaming, and 360° video processing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 08:42:44 -06:00

344 lines
12 KiB
Python

"""Tests for advanced video encoders (AV1, HEVC, HDR)."""
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from video_processor.config import ProcessorConfig
from video_processor.core.advanced_encoders import AdvancedVideoEncoder, HDRProcessor
from video_processor.exceptions import EncodingError, FFmpegError
class TestAdvancedVideoEncoder:
"""Test advanced video encoder functionality."""
def test_initialization(self):
"""Test advanced encoder initialization."""
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
assert encoder.config == config
assert encoder._quality_presets is not None
def test_get_advanced_quality_presets(self):
"""Test advanced quality presets configuration."""
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
presets = encoder._get_advanced_quality_presets()
assert "low" in presets
assert "medium" in presets
assert "high" in presets
assert "ultra" in presets
# Check AV1-specific parameters
assert "av1_crf" in presets["medium"]
assert "av1_cpu_used" in presets["medium"]
assert "bitrate_multiplier" in presets["medium"]
@patch("subprocess.run")
def test_check_av1_support_available(self, mock_run):
"""Test AV1 support detection when available."""
# Mock ffmpeg -encoders output with AV1 support
mock_run.return_value = Mock(
returncode=0, stdout="... libaom-av1 ... AV1 encoder ...", stderr=""
)
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
result = encoder._check_av1_support()
assert result is True
mock_run.assert_called_once()
@patch("subprocess.run")
def test_check_av1_support_unavailable(self, mock_run):
"""Test AV1 support detection when unavailable."""
# Mock ffmpeg -encoders output without AV1 support
mock_run.return_value = Mock(
returncode=0, stdout="libx264 libx265 libvpx-vp9", stderr=""
)
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
result = encoder._check_av1_support()
assert result is False
@patch("subprocess.run")
def test_check_hardware_hevc_support(self, mock_run):
"""Test hardware HEVC support detection."""
# Mock ffmpeg -encoders output with hardware HEVC support
mock_run.return_value = Mock(
returncode=0, stdout="... hevc_nvenc ... NVIDIA HEVC encoder ...", stderr=""
)
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
result = encoder._check_hardware_hevc_support()
assert result is True
@patch(
"video_processor.core.advanced_encoders.AdvancedVideoEncoder._check_av1_support"
)
@patch("video_processor.core.advanced_encoders.subprocess.run")
def test_encode_av1_mp4_success(self, mock_run, mock_av1_support):
"""Test successful AV1 MP4 encoding."""
# Mock AV1 support as available
mock_av1_support.return_value = True
# Mock successful subprocess runs for two-pass encoding
mock_run.side_effect = [
Mock(returncode=0, stderr=""), # Pass 1
Mock(returncode=0, stderr=""), # Pass 2
]
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
# Mock file operations - output file exists, log files don't
with (
patch("pathlib.Path.exists", return_value=True),
patch("pathlib.Path.unlink") as mock_unlink,
):
result = encoder.encode_av1(
Path("input.mp4"), Path("/output"), "test_id", container="mp4"
)
assert result == Path("/output/test_id_av1.mp4")
assert mock_run.call_count == 2 # Two-pass encoding
@patch(
"video_processor.core.advanced_encoders.AdvancedVideoEncoder._check_av1_support"
)
def test_encode_av1_no_support(self, mock_av1_support):
"""Test AV1 encoding when support is unavailable."""
# Mock AV1 support as unavailable
mock_av1_support.return_value = False
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
with pytest.raises(EncodingError, match="AV1 encoding requires libaom-av1"):
encoder.encode_av1(Path("input.mp4"), Path("/output"), "test_id")
@patch(
"video_processor.core.advanced_encoders.AdvancedVideoEncoder._check_av1_support"
)
@patch("video_processor.core.advanced_encoders.subprocess.run")
def test_encode_av1_single_pass(self, mock_run, mock_av1_support):
"""Test AV1 single-pass encoding."""
mock_av1_support.return_value = True
mock_run.return_value = Mock(returncode=0, stderr="")
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
with (
patch("pathlib.Path.exists", return_value=True),
patch("pathlib.Path.unlink"),
):
result = encoder.encode_av1(
Path("input.mp4"), Path("/output"), "test_id", use_two_pass=False
)
assert result == Path("/output/test_id_av1.mp4")
assert mock_run.call_count == 1 # Single-pass encoding
@patch(
"video_processor.core.advanced_encoders.AdvancedVideoEncoder._check_av1_support"
)
@patch("video_processor.core.advanced_encoders.subprocess.run")
def test_encode_av1_webm_container(self, mock_run, mock_av1_support):
"""Test AV1 encoding with WebM container."""
mock_av1_support.return_value = True
mock_run.side_effect = [
Mock(returncode=0, stderr=""), # Pass 1
Mock(returncode=0, stderr=""), # Pass 2
]
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
with (
patch("pathlib.Path.exists", return_value=True),
patch("pathlib.Path.unlink"),
):
result = encoder.encode_av1(
Path("input.mp4"), Path("/output"), "test_id", container="webm"
)
assert result == Path("/output/test_id_av1.webm")
@patch(
"video_processor.core.advanced_encoders.AdvancedVideoEncoder._check_av1_support"
)
@patch("video_processor.core.advanced_encoders.subprocess.run")
def test_encode_av1_encoding_failure(self, mock_run, mock_av1_support):
"""Test AV1 encoding failure handling."""
mock_av1_support.return_value = True
mock_run.return_value = Mock(returncode=1, stderr="Encoding failed")
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
with pytest.raises(FFmpegError, match="AV1 Pass 1 failed"):
encoder.encode_av1(Path("input.mp4"), Path("/output"), "test_id")
@patch("subprocess.run")
def test_encode_hevc_success(self, mock_run):
"""Test successful HEVC encoding."""
mock_run.return_value = Mock(returncode=0, stderr="")
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
with patch("pathlib.Path.exists", return_value=True):
result = encoder.encode_hevc(Path("input.mp4"), Path("/output"), "test_id")
assert result == Path("/output/test_id_hevc.mp4")
@patch(
"video_processor.core.advanced_encoders.AdvancedVideoEncoder._check_hardware_hevc_support"
)
@patch("subprocess.run")
def test_encode_hevc_hardware_fallback(self, mock_run, mock_hw_support):
"""Test HEVC hardware encoding with software fallback."""
mock_hw_support.return_value = True
# First call (hardware) fails, second call (software) succeeds
mock_run.side_effect = [
Mock(returncode=1, stderr="Hardware encoding failed"), # Hardware fails
Mock(returncode=0, stderr=""), # Software succeeds
]
config = ProcessorConfig()
encoder = AdvancedVideoEncoder(config)
with patch("pathlib.Path.exists", return_value=True):
result = encoder.encode_hevc(
Path("input.mp4"), Path("/output"), "test_id", use_hardware=True
)
assert result == Path("/output/test_id_hevc.mp4")
assert mock_run.call_count == 2 # Hardware + fallback
def test_get_av1_bitrate_multiplier(self):
"""Test AV1 bitrate multiplier calculation."""
config = ProcessorConfig(quality_preset="medium")
encoder = AdvancedVideoEncoder(config)
multiplier = encoder.get_av1_bitrate_multiplier()
assert isinstance(multiplier, float)
assert 0.5 <= multiplier <= 1.0 # AV1 should use less bitrate
def test_get_supported_advanced_codecs(self):
"""Test advanced codec support reporting."""
codecs = AdvancedVideoEncoder.get_supported_advanced_codecs()
assert isinstance(codecs, dict)
assert "av1" in codecs
assert "hevc" in codecs
assert "hardware_hevc" in codecs
class TestHDRProcessor:
"""Test HDR video processing functionality."""
def test_initialization(self):
"""Test HDR processor initialization."""
config = ProcessorConfig()
processor = HDRProcessor(config)
assert processor.config == config
@patch("subprocess.run")
def test_encode_hdr_hevc_success(self, mock_run):
"""Test successful HDR HEVC encoding."""
mock_run.return_value = Mock(returncode=0, stderr="")
config = ProcessorConfig()
processor = HDRProcessor(config)
with patch("pathlib.Path.exists", return_value=True):
result = processor.encode_hdr_hevc(
Path("input_hdr.mp4"), Path("/output"), "test_id"
)
assert result == Path("/output/test_id_hdr_hdr10.mp4")
mock_run.assert_called_once()
# Check that HDR parameters were included in the command
call_args = mock_run.call_args[0][0]
assert "-color_primaries" in call_args
assert "bt2020" in call_args
@patch("subprocess.run")
def test_encode_hdr_hevc_failure(self, mock_run):
"""Test HDR HEVC encoding failure."""
mock_run.return_value = Mock(returncode=1, stderr="HDR encoding failed")
config = ProcessorConfig()
processor = HDRProcessor(config)
with pytest.raises(FFmpegError, match="HDR encoding failed"):
processor.encode_hdr_hevc(Path("input_hdr.mp4"), Path("/output"), "test_id")
@patch("subprocess.run")
def test_analyze_hdr_content_hdr_video(self, mock_run):
"""Test HDR content analysis for HDR video."""
# Mock ffprobe output indicating HDR content
mock_run.return_value = Mock(returncode=0, stdout="bt2020,smpte2084,bt2020nc\n")
config = ProcessorConfig()
processor = HDRProcessor(config)
result = processor.analyze_hdr_content(Path("hdr_video.mp4"))
assert result["is_hdr"] is True
assert result["color_primaries"] == "bt2020"
assert result["color_transfer"] == "smpte2084"
@patch("subprocess.run")
def test_analyze_hdr_content_sdr_video(self, mock_run):
"""Test HDR content analysis for SDR video."""
# Mock ffprobe output indicating SDR content
mock_run.return_value = Mock(returncode=0, stdout="bt709,bt709,bt709\n")
config = ProcessorConfig()
processor = HDRProcessor(config)
result = processor.analyze_hdr_content(Path("sdr_video.mp4"))
assert result["is_hdr"] is False
assert result["color_primaries"] == "bt709"
@patch("subprocess.run")
def test_analyze_hdr_content_failure(self, mock_run):
"""Test HDR content analysis failure handling."""
mock_run.return_value = Mock(returncode=1, stderr="Analysis failed")
config = ProcessorConfig()
processor = HDRProcessor(config)
result = processor.analyze_hdr_content(Path("video.mp4"))
assert result["is_hdr"] is False
assert "error" in result
def test_get_hdr_support(self):
"""Test HDR support reporting."""
support = HDRProcessor.get_hdr_support()
assert isinstance(support, dict)
assert "hdr10" in support
assert "hdr10plus" in support
assert "dolby_vision" in support