155 lines
4.8 KiB
Python
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
|