Chore: rename api->camera-management-api and web->management-dashboard-web-app; update compose, ignore, README references
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Video Streaming Application Service.
|
||||
|
||||
Handles video streaming use cases including range requests and caching.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ..domain.interfaces import VideoRepository, StreamingCache
|
||||
from ..domain.models import VideoFile, StreamRange
|
||||
|
||||
|
||||
class StreamingService:
|
||||
"""Application service for video streaming"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
video_repository: VideoRepository,
|
||||
streaming_cache: Optional[StreamingCache] = None
|
||||
):
|
||||
self.video_repository = video_repository
|
||||
self.streaming_cache = streaming_cache
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def stream_video_range(
|
||||
self,
|
||||
file_id: str,
|
||||
range_request: Optional[StreamRange] = None
|
||||
) -> Tuple[Optional[bytes], Optional[VideoFile], Optional[StreamRange]]:
|
||||
"""
|
||||
Stream video data for a specific range.
|
||||
|
||||
Returns:
|
||||
Tuple of (data, video_file, actual_range)
|
||||
"""
|
||||
try:
|
||||
# Get video file
|
||||
video_file = await self.video_repository.get_by_id(file_id)
|
||||
if not video_file or not video_file.is_streamable:
|
||||
return None, None, None
|
||||
|
||||
# If no range specified, create range for entire file
|
||||
if range_request is None:
|
||||
range_request = StreamRange(start=0, end=video_file.file_size_bytes - 1)
|
||||
|
||||
# Validate and adjust range
|
||||
actual_range = self._validate_range(range_request, video_file.file_size_bytes)
|
||||
if not actual_range:
|
||||
return None, video_file, None
|
||||
|
||||
# Try to get from cache first
|
||||
if self.streaming_cache:
|
||||
cached_data = await self.streaming_cache.get_cached_range(file_id, actual_range)
|
||||
if cached_data:
|
||||
self.logger.debug(f"Serving cached range for {file_id}")
|
||||
return cached_data, video_file, actual_range
|
||||
|
||||
# Read from file
|
||||
data = await self.video_repository.get_file_range(video_file, actual_range)
|
||||
|
||||
# Cache the data if caching is enabled
|
||||
if self.streaming_cache and data:
|
||||
await self.streaming_cache.cache_range(file_id, actual_range, data)
|
||||
|
||||
return data, video_file, actual_range
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error streaming video range for {file_id}: {e}")
|
||||
return None, None, None
|
||||
|
||||
async def get_video_info(self, file_id: str) -> Optional[VideoFile]:
|
||||
"""Get video information for streaming"""
|
||||
try:
|
||||
video_file = await self.video_repository.get_by_id(file_id)
|
||||
if not video_file or not video_file.is_streamable:
|
||||
return None
|
||||
|
||||
return video_file
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting video info for {file_id}: {e}")
|
||||
return None
|
||||
|
||||
async def invalidate_cache(self, file_id: str) -> bool:
|
||||
"""Invalidate cached data for a video file"""
|
||||
try:
|
||||
if self.streaming_cache:
|
||||
await self.streaming_cache.invalidate_file(file_id)
|
||||
self.logger.info(f"Invalidated cache for {file_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error invalidating cache for {file_id}: {e}")
|
||||
return False
|
||||
|
||||
async def cleanup_cache(self, max_size_mb: int = 100) -> int:
|
||||
"""Clean up streaming cache"""
|
||||
try:
|
||||
if self.streaming_cache:
|
||||
return await self.streaming_cache.cleanup_cache(max_size_mb)
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error cleaning up cache: {e}")
|
||||
return 0
|
||||
|
||||
def _validate_range(self, range_request: StreamRange, file_size: int) -> Optional[StreamRange]:
|
||||
"""Validate and adjust range request for file size"""
|
||||
try:
|
||||
start = range_request.start
|
||||
end = range_request.end
|
||||
|
||||
# Validate start position
|
||||
if start < 0:
|
||||
start = 0
|
||||
elif start >= file_size:
|
||||
return None
|
||||
|
||||
# Validate end position
|
||||
if end is None or end >= file_size:
|
||||
end = file_size - 1
|
||||
elif end < start:
|
||||
return None
|
||||
|
||||
return StreamRange(start=start, end=end)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error validating range: {e}")
|
||||
return None
|
||||
|
||||
def calculate_content_range_header(
|
||||
self,
|
||||
range_request: StreamRange,
|
||||
file_size: int
|
||||
) -> str:
|
||||
"""Calculate Content-Range header value"""
|
||||
return f"bytes {range_request.start}-{range_request.end}/{file_size}"
|
||||
|
||||
def should_use_partial_content(self, range_request: Optional[StreamRange], file_size: int) -> bool:
|
||||
"""Determine if response should use 206 Partial Content"""
|
||||
if not range_request:
|
||||
return False
|
||||
|
||||
# Use partial content if not requesting the entire file
|
||||
return not (range_request.start == 0 and range_request.end == file_size - 1)
|
||||
|
||||
async def get_optimal_chunk_size(self, file_size: int) -> int:
|
||||
"""Get optimal chunk size for streaming based on file size"""
|
||||
# Adaptive chunk sizing
|
||||
if file_size < 1024 * 1024: # < 1MB
|
||||
return 64 * 1024 # 64KB chunks
|
||||
elif file_size < 10 * 1024 * 1024: # < 10MB
|
||||
return 256 * 1024 # 256KB chunks
|
||||
elif file_size < 100 * 1024 * 1024: # < 100MB
|
||||
return 512 * 1024 # 512KB chunks
|
||||
else:
|
||||
return 1024 * 1024 # 1MB chunks for large files
|
||||
Reference in New Issue
Block a user