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>
322 lines
12 KiB
Python
322 lines
12 KiB
Python
"""Tests for adaptive streaming functionality."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
from video_processor.config import ProcessorConfig
|
|
from video_processor.streaming.adaptive import (
|
|
AdaptiveStreamProcessor,
|
|
BitrateLevel,
|
|
StreamingPackage,
|
|
)
|
|
|
|
|
|
class TestBitrateLevel:
|
|
"""Test BitrateLevel dataclass."""
|
|
|
|
def test_bitrate_level_creation(self):
|
|
"""Test BitrateLevel creation."""
|
|
level = BitrateLevel(
|
|
name="720p",
|
|
width=1280,
|
|
height=720,
|
|
bitrate=3000,
|
|
max_bitrate=4500,
|
|
codec="h264",
|
|
container="mp4",
|
|
)
|
|
|
|
assert level.name == "720p"
|
|
assert level.width == 1280
|
|
assert level.height == 720
|
|
assert level.bitrate == 3000
|
|
assert level.max_bitrate == 4500
|
|
assert level.codec == "h264"
|
|
assert level.container == "mp4"
|
|
|
|
|
|
class TestStreamingPackage:
|
|
"""Test StreamingPackage dataclass."""
|
|
|
|
def test_streaming_package_creation(self):
|
|
"""Test StreamingPackage creation."""
|
|
package = StreamingPackage(
|
|
video_id="test_video",
|
|
source_path=Path("input.mp4"),
|
|
output_dir=Path("/output"),
|
|
segment_duration=6,
|
|
)
|
|
|
|
assert package.video_id == "test_video"
|
|
assert package.source_path == Path("input.mp4")
|
|
assert package.output_dir == Path("/output")
|
|
assert package.segment_duration == 6
|
|
assert package.hls_playlist is None
|
|
assert package.dash_manifest is None
|
|
|
|
|
|
class TestAdaptiveStreamProcessor:
|
|
"""Test adaptive stream processor functionality."""
|
|
|
|
def test_initialization(self):
|
|
"""Test processor initialization."""
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
assert processor.config == config
|
|
assert processor.enable_ai_optimization in [
|
|
True,
|
|
False,
|
|
] # Depends on AI availability
|
|
|
|
def test_initialization_with_ai_disabled(self):
|
|
"""Test processor initialization with AI disabled."""
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config, enable_ai_optimization=False)
|
|
|
|
assert processor.enable_ai_optimization is False
|
|
assert processor.content_analyzer is None
|
|
|
|
def test_get_streaming_capabilities(self):
|
|
"""Test streaming capabilities reporting."""
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
capabilities = processor.get_streaming_capabilities()
|
|
|
|
assert isinstance(capabilities, dict)
|
|
assert "hls_streaming" in capabilities
|
|
assert "dash_streaming" in capabilities
|
|
assert "ai_optimization" in capabilities
|
|
assert "advanced_codecs" in capabilities
|
|
assert "thumbnail_tracks" in capabilities
|
|
assert "multi_bitrate" in capabilities
|
|
|
|
def test_get_output_format_mapping(self):
|
|
"""Test codec to output format mapping."""
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
assert processor._get_output_format("h264") == "mp4"
|
|
assert processor._get_output_format("hevc") == "hevc"
|
|
assert processor._get_output_format("av1") == "av1_mp4"
|
|
assert processor._get_output_format("unknown") == "mp4"
|
|
|
|
def test_get_quality_preset_for_bitrate(self):
|
|
"""Test quality preset selection based on bitrate."""
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
assert processor._get_quality_preset_for_bitrate(500) == "low"
|
|
assert processor._get_quality_preset_for_bitrate(2000) == "medium"
|
|
assert processor._get_quality_preset_for_bitrate(5000) == "high"
|
|
assert processor._get_quality_preset_for_bitrate(10000) == "ultra"
|
|
|
|
def test_get_ffmpeg_options_for_level(self):
|
|
"""Test FFmpeg options generation for bitrate levels."""
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
level = BitrateLevel(
|
|
name="720p",
|
|
width=1280,
|
|
height=720,
|
|
bitrate=3000,
|
|
max_bitrate=4500,
|
|
codec="h264",
|
|
container="mp4",
|
|
)
|
|
|
|
options = processor._get_ffmpeg_options_for_level(level)
|
|
|
|
assert options["b:v"] == "3000k"
|
|
assert options["maxrate"] == "4500k"
|
|
assert options["bufsize"] == "9000k"
|
|
assert options["s"] == "1280x720"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_optimal_bitrate_ladder_without_ai(self):
|
|
"""Test bitrate ladder generation without AI analysis."""
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config, enable_ai_optimization=False)
|
|
|
|
levels = await processor._generate_optimal_bitrate_ladder(Path("test.mp4"))
|
|
|
|
assert isinstance(levels, list)
|
|
assert len(levels) >= 1
|
|
assert all(isinstance(level, BitrateLevel) for level in levels)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("video_processor.streaming.adaptive.VideoContentAnalyzer")
|
|
async def test_generate_optimal_bitrate_ladder_with_ai(self, mock_analyzer_class):
|
|
"""Test bitrate ladder generation with AI analysis."""
|
|
# Mock AI analyzer
|
|
mock_analyzer = Mock()
|
|
mock_analysis = Mock()
|
|
mock_analysis.resolution = (1920, 1080)
|
|
mock_analysis.motion_intensity = 0.8
|
|
mock_analyzer.analyze_content = AsyncMock(return_value=mock_analysis)
|
|
mock_analyzer_class.return_value = mock_analyzer
|
|
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config, enable_ai_optimization=True)
|
|
processor.content_analyzer = mock_analyzer
|
|
|
|
levels = await processor._generate_optimal_bitrate_ladder(Path("test.mp4"))
|
|
|
|
assert isinstance(levels, list)
|
|
assert len(levels) >= 1
|
|
|
|
# Check that bitrates were adjusted for high motion
|
|
for level in levels:
|
|
assert level.bitrate > 0
|
|
assert level.max_bitrate > level.bitrate
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("video_processor.streaming.adaptive.VideoProcessor")
|
|
@patch("video_processor.streaming.adaptive.asyncio.to_thread")
|
|
async def test_generate_bitrate_renditions(
|
|
self, mock_to_thread, mock_processor_class
|
|
):
|
|
"""Test bitrate rendition generation."""
|
|
# Mock VideoProcessor
|
|
mock_result = Mock()
|
|
mock_result.encoded_files = {"mp4": Path("/output/test.mp4")}
|
|
mock_processor_instance = Mock()
|
|
mock_processor_instance.process_video.return_value = mock_result
|
|
mock_processor_class.return_value = mock_processor_instance
|
|
mock_to_thread.return_value = mock_result
|
|
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
bitrate_levels = [
|
|
BitrateLevel("480p", 854, 480, 1500, 2250, "h264", "mp4"),
|
|
BitrateLevel("720p", 1280, 720, 3000, 4500, "h264", "mp4"),
|
|
]
|
|
|
|
with patch("pathlib.Path.mkdir"):
|
|
rendition_files = await processor._generate_bitrate_renditions(
|
|
Path("input.mp4"), Path("/output"), "test_video", bitrate_levels
|
|
)
|
|
|
|
assert isinstance(rendition_files, dict)
|
|
assert len(rendition_files) == 2
|
|
assert "480p" in rendition_files
|
|
assert "720p" in rendition_files
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("video_processor.streaming.adaptive.asyncio.to_thread")
|
|
async def test_generate_thumbnail_track(self, mock_to_thread):
|
|
"""Test thumbnail track generation."""
|
|
# Mock VideoProcessor result
|
|
mock_result = Mock()
|
|
mock_result.sprite_file = Path("/output/sprite.jpg")
|
|
mock_to_thread.return_value = mock_result
|
|
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
with patch("video_processor.streaming.adaptive.VideoProcessor"):
|
|
thumbnail_track = await processor._generate_thumbnail_track(
|
|
Path("input.mp4"), Path("/output"), "test_video"
|
|
)
|
|
|
|
assert thumbnail_track == Path("/output/sprite.jpg")
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("video_processor.streaming.adaptive.asyncio.to_thread")
|
|
async def test_generate_thumbnail_track_failure(self, mock_to_thread):
|
|
"""Test thumbnail track generation failure."""
|
|
mock_to_thread.side_effect = Exception("Thumbnail generation failed")
|
|
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
with patch("video_processor.streaming.adaptive.VideoProcessor"):
|
|
thumbnail_track = await processor._generate_thumbnail_track(
|
|
Path("input.mp4"), Path("/output"), "test_video"
|
|
)
|
|
|
|
assert thumbnail_track is None
|
|
|
|
@pytest.mark.asyncio
|
|
@patch(
|
|
"video_processor.streaming.adaptive.AdaptiveStreamProcessor._generate_hls_playlist"
|
|
)
|
|
@patch(
|
|
"video_processor.streaming.adaptive.AdaptiveStreamProcessor._generate_dash_manifest"
|
|
)
|
|
@patch(
|
|
"video_processor.streaming.adaptive.AdaptiveStreamProcessor._generate_thumbnail_track"
|
|
)
|
|
@patch(
|
|
"video_processor.streaming.adaptive.AdaptiveStreamProcessor._generate_bitrate_renditions"
|
|
)
|
|
@patch(
|
|
"video_processor.streaming.adaptive.AdaptiveStreamProcessor._generate_optimal_bitrate_ladder"
|
|
)
|
|
async def test_create_adaptive_stream(
|
|
self, mock_ladder, mock_renditions, mock_thumbnail, mock_dash, mock_hls
|
|
):
|
|
"""Test complete adaptive stream creation."""
|
|
# Setup mocks
|
|
mock_bitrate_levels = [
|
|
BitrateLevel("720p", 1280, 720, 3000, 4500, "h264", "mp4")
|
|
]
|
|
mock_rendition_files = {"720p": Path("/output/720p.mp4")}
|
|
|
|
mock_ladder.return_value = mock_bitrate_levels
|
|
mock_renditions.return_value = mock_rendition_files
|
|
mock_thumbnail.return_value = Path("/output/sprite.jpg")
|
|
mock_hls.return_value = Path("/output/playlist.m3u8")
|
|
mock_dash.return_value = Path("/output/manifest.mpd")
|
|
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
with patch("pathlib.Path.mkdir"):
|
|
result = await processor.create_adaptive_stream(
|
|
Path("input.mp4"), Path("/output"), "test_video", ["hls", "dash"]
|
|
)
|
|
|
|
assert isinstance(result, StreamingPackage)
|
|
assert result.video_id == "test_video"
|
|
assert result.hls_playlist == Path("/output/playlist.m3u8")
|
|
assert result.dash_manifest == Path("/output/manifest.mpd")
|
|
assert result.thumbnail_track == Path("/output/sprite.jpg")
|
|
assert result.bitrate_levels == mock_bitrate_levels
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_adaptive_stream_with_custom_ladder(self):
|
|
"""Test adaptive stream creation with custom bitrate ladder."""
|
|
custom_levels = [
|
|
BitrateLevel("480p", 854, 480, 1500, 2250, "h264", "mp4"),
|
|
]
|
|
|
|
config = ProcessorConfig()
|
|
processor = AdaptiveStreamProcessor(config)
|
|
|
|
with (
|
|
patch.multiple(
|
|
processor,
|
|
_generate_bitrate_renditions=AsyncMock(
|
|
return_value={"480p": Path("test.mp4")}
|
|
),
|
|
_generate_hls_playlist=AsyncMock(return_value=Path("playlist.m3u8")),
|
|
_generate_dash_manifest=AsyncMock(return_value=Path("manifest.mpd")),
|
|
_generate_thumbnail_track=AsyncMock(return_value=Path("sprite.jpg")),
|
|
),
|
|
patch("pathlib.Path.mkdir"),
|
|
):
|
|
result = await processor.create_adaptive_stream(
|
|
Path("input.mp4"),
|
|
Path("/output"),
|
|
"test_video",
|
|
custom_bitrate_ladder=custom_levels,
|
|
)
|
|
|
|
assert result.bitrate_levels == custom_levels
|