""" Video processing and validation utilities. This module provides video file validation, metadata extraction, and optional thumbnail generation for uploaded videos. """ import io import os import tempfile import ffmpeg import mimetypes from typing import Optional, Tuple from django.core.exceptions import ValidationError # Allowed video MIME types ALLOWED_VIDEO_TYPES = { 'video/mp4', 'video/quicktime', # .mov 'video/x-msvideo', # .avi 'video/webm', 'video/x-matroska', # .mkv } # Maximum video file size (250 MB) MAX_VIDEO_SIZE = 250 * 1024 * 1024 def verify_video_bytes(data: bytes, filename: str = "") -> str: """ Verify the uploaded bytes are a valid video file. Uses MIME type detection to validate the file format. For more thorough validation, install python-magic or ffmpeg-python. Args: data: The uploaded file bytes filename: Original filename for extension-based fallback Returns: str: The detected content type (MIME type) Raises: ValidationError: If the file is not a valid video or exceeds size limits """ if not data: raise ValidationError("Uploaded file is empty.") # Check file size if len(data) > MAX_VIDEO_SIZE: size_mb = len(data) / (1024 * 1024) max_mb = MAX_VIDEO_SIZE / (1024 * 1024) raise ValidationError( f"Video file too large ({size_mb:.1f} MB). Maximum size is {max_mb:.0f} MB." ) # Try to detect MIME type from file extension content_type = None if filename: content_type, _ = mimetypes.guess_type(filename) # Basic validation: check if it looks like a video MIME type if not content_type or not content_type.startswith('video/'): raise ValidationError( "Uploaded file does not appear to be a video. " "Supported formats: MP4, MOV, WebM, AVI, MKV" ) if content_type not in ALLOWED_VIDEO_TYPES: raise ValidationError( f"Video format '{content_type}' is not allowed. " f"Supported formats: {', '.join(ALLOWED_VIDEO_TYPES)}" ) return content_type def extract_video_metadata(video_path: str) -> Optional[Tuple[int, int, int]]: """ Extract video metadata (width, height, duration) from a video file. Uses ffmpeg to probe the video file and extract dimensions and duration. Args: video_path: Path to the video file on disk Returns: Optional[Tuple[int, int, int]]: (width, height, duration_seconds) or None if extraction fails """ try: probe = ffmpeg.probe(video_path) # Find the first video stream video_stream = next( (stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None ) if not video_stream: return None width = int(video_stream.get('width', 0)) height = int(video_stream.get('height', 0)) # Duration can be in the stream or format section duration = video_stream.get('duration') or probe.get('format', {}).get('duration') duration_seconds = int(float(duration)) if duration else 0 return (width, height, duration_seconds) except (ffmpeg.Error, KeyError, ValueError, StopIteration) as e: # If extraction fails, return None and let fields default to 0 return None def generate_video_thumbnail(video_path: str, output_path: str, timestamp: float = 1.0) -> bool: """ Generate a thumbnail image from a video file. Uses ffmpeg to extract a frame from the video at the specified timestamp and save it as a JPEG thumbnail scaled to 320px width. Args: video_path: Path to the video file output_path: Path where thumbnail should be saved (should end in .jpg) timestamp: Time in seconds to extract frame from (default 1.0) Returns: bool: True if successful, False otherwise """ try: # Ensure output directory exists output_dir = os.path.dirname(output_path) if output_dir and not os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) # Extract frame at timestamp, scale to 320px width (preserve aspect ratio), save as JPEG ( ffmpeg .input(video_path, ss=timestamp) .filter('scale', 320, -1) # -1 maintains aspect ratio .output(output_path, vframes=1, format='image2', vcodec='mjpeg') .overwrite_output() .run(capture_stdout=True, capture_stderr=True, quiet=True) ) return os.path.exists(output_path) except ffmpeg.Error as e: # FFmpeg error (corrupt video, invalid timestamp, etc.) return False except Exception as e: # Other errors (permissions, disk space, etc.) return False