2026-01-26 11:09:40 -05:00

155 lines
4.8 KiB
Python

"""
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