Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,4 @@ k8s/*/secrets/
local/
dev/
development/
data/
91 changes: 80 additions & 11 deletions api/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,12 +635,15 @@ def validate_concat_operation(op: Dict[str, Any]) -> Dict[str, Any]:
def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
"""Validate transcode operation with enhanced security checks."""
validated = {"type": "transcode"}

# Allowed video codecs
ALLOWED_VIDEO_CODECS = {'h264', 'h265', 'hevc', 'vp8', 'vp9', 'av1', 'libx264', 'libx265', 'copy'}
ALLOWED_AUDIO_CODECS = {'aac', 'mp3', 'opus', 'vorbis', 'ac3', 'libfdk_aac', 'copy'}
ALLOWED_VIDEO_CODECS = {'h264', 'h265', 'hevc', 'vp8', 'vp9', 'av1', 'libx264', 'libx265', 'copy', 'prores', 'dnxhd'}
ALLOWED_AUDIO_CODECS = {'aac', 'mp3', 'opus', 'vorbis', 'ac3', 'eac3', 'libfdk_aac', 'flac', 'pcm_s16le', 'pcm_s24le', 'copy'}
ALLOWED_PRESETS = {'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'}

ALLOWED_PROFILES = {'baseline', 'main', 'high', 'high10', 'high422', 'high444'}
ALLOWED_PIXEL_FORMATS = {'yuv420p', 'yuv422p', 'yuv444p', 'yuv420p10le', 'yuv422p10le', 'rgb24', 'rgba'}
ALLOWED_HW_ACCEL = {'auto', 'none', 'nvenc', 'qsv', 'vaapi', 'videotoolbox'}

# Validate video codec
if "video_codec" in op:
codec = op["video_codec"]
Expand All @@ -649,7 +652,7 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
if codec not in ALLOWED_VIDEO_CODECS:
raise ValueError(f"Invalid video codec: {codec}")
validated["video_codec"] = codec

# Validate audio codec
if "audio_codec" in op:
codec = op["audio_codec"]
Expand All @@ -658,7 +661,7 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
if codec not in ALLOWED_AUDIO_CODECS:
raise ValueError(f"Invalid audio codec: {codec}")
validated["audio_codec"] = codec

# Validate preset
if "preset" in op:
preset = op["preset"]
Expand All @@ -667,21 +670,48 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
if preset not in ALLOWED_PRESETS:
raise ValueError(f"Invalid preset: {preset}")
validated["preset"] = preset


# Validate profile (for H.264/H.265)
if "profile" in op:
profile = op["profile"]
if not isinstance(profile, str):
raise ValueError("Profile must be a string")
if profile not in ALLOWED_PROFILES:
raise ValueError(f"Invalid profile: {profile}")
validated["profile"] = profile

# Validate pixel format
if "pixel_format" in op or "pix_fmt" in op:
pix_fmt = op.get("pixel_format") or op.get("pix_fmt")
if pix_fmt not in ALLOWED_PIXEL_FORMATS:
raise ValueError(f"Invalid pixel format: {pix_fmt}")
validated["pixel_format"] = pix_fmt

# Validate hardware acceleration
if "hardware_acceleration" in op or "hw_accel" in op:
hw = op.get("hardware_acceleration") or op.get("hw_accel")
if hw not in ALLOWED_HW_ACCEL:
raise ValueError(f"Invalid hardware acceleration: {hw}")
validated["hardware_acceleration"] = hw

# Validate bitrates
if "video_bitrate" in op:
validated["video_bitrate"] = validate_bitrate(op["video_bitrate"])
if "audio_bitrate" in op:
validated["audio_bitrate"] = validate_bitrate(op["audio_bitrate"])

if "max_bitrate" in op:
validated["max_bitrate"] = validate_bitrate(op["max_bitrate"])
if "buffer_size" in op:
validated["buffer_size"] = validate_bitrate(op["buffer_size"])

# Validate resolution
if "width" in op or "height" in op:
width = op.get("width")
height = op.get("height")
validated_resolution = validate_resolution(width, height)
if validated_resolution:
validated.update(validated_resolution)

# Validate frame rate
if "fps" in op:
fps = op["fps"]
Expand All @@ -691,7 +721,7 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
validated["fps"] = float(fps)
else:
raise ValueError("FPS must be a number")

# Validate CRF
if "crf" in op:
crf = op["crf"]
Expand All @@ -701,7 +731,46 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
validated["crf"] = int(crf)
else:
raise ValueError("CRF must be a number")


# Validate GOP size (keyframe interval)
if "gop_size" in op or "keyint" in op:
gop = op.get("gop_size") or op.get("keyint")
if isinstance(gop, int):
if gop < 1 or gop > 600:
raise ValueError("GOP size out of valid range (1-600)")
validated["gop_size"] = gop
else:
raise ValueError("GOP size must be an integer")

# Validate B-frames
if "b_frames" in op or "bframes" in op:
bf = op.get("b_frames") or op.get("bframes")
if isinstance(bf, int):
if bf < 0 or bf > 16:
raise ValueError("B-frames out of valid range (0-16)")
validated["b_frames"] = bf
else:
raise ValueError("B-frames must be an integer")

# Validate two-pass encoding
if "two_pass" in op:
validated["two_pass"] = bool(op["two_pass"])

# Validate audio sample rate
if "audio_sample_rate" in op:
sr = op["audio_sample_rate"]
allowed_rates = [8000, 11025, 16000, 22050, 32000, 44100, 48000, 96000]
if sr not in allowed_rates:
raise ValueError(f"Invalid audio sample rate: {sr}")
validated["audio_sample_rate"] = sr

# Validate audio channels
if "audio_channels" in op:
channels = op["audio_channels"]
if channels not in [1, 2, 6, 8]:
raise ValueError("Audio channels must be 1, 2, 6, or 8")
validated["audio_channels"] = channels

return validated


Expand Down
100 changes: 78 additions & 22 deletions worker/utils/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,50 +518,97 @@ def _validate_time_string(self, time_str: str, param_name: str):
def _handle_transcode(self, params: Dict[str, Any]) -> List[str]:
"""Handle video transcoding parameters."""
cmd_parts = []


# Hardware acceleration preference
hw_pref = params.get('hardware_acceleration', 'auto')

# Video codec
if 'video_codec' in params:
codec = params['video_codec']
encoder = HardwareAcceleration.get_best_encoder(codec, self.hardware_caps)
if hw_pref == 'none' or codec == 'copy':
# Use software encoder or copy
encoder = 'copy' if codec == 'copy' else f"lib{codec}" if codec in ('x264', 'x265') else codec
else:
encoder = HardwareAcceleration.get_best_encoder(codec, self.hardware_caps)
cmd_parts.extend(['-c:v', encoder])

# Audio codec
if 'audio_codec' in params:
cmd_parts.extend(['-c:a', params['audio_codec']])
# Bitrate

# Video bitrate with VBV buffer
if 'video_bitrate' in params:
cmd_parts.extend(['-b:v', params['video_bitrate']])
cmd_parts.extend(['-b:v', str(params['video_bitrate'])])
if 'max_bitrate' in params:
cmd_parts.extend(['-maxrate', str(params['max_bitrate'])])
if 'buffer_size' in params:
cmd_parts.extend(['-bufsize', str(params['buffer_size'])])

# Audio bitrate
if 'audio_bitrate' in params:
cmd_parts.extend(['-b:a', params['audio_bitrate']])
cmd_parts.extend(['-b:a', str(params['audio_bitrate'])])

# Resolution
if 'width' in params and 'height' in params:
cmd_parts.extend(['-s', f"{params['width']}x{params['height']}"])

# Frame rate
if 'fps' in params:
cmd_parts.extend(['-r', str(params['fps'])])

# Quality settings
if 'crf' in params:
cmd_parts.extend(['-crf', str(params['crf'])])
if 'preset' in params:
cmd_parts.extend(['-preset', params['preset']])


# Profile (H.264/H.265)
if 'profile' in params:
cmd_parts.extend(['-profile:v', params['profile']])

# Pixel format
if 'pixel_format' in params:
cmd_parts.extend(['-pix_fmt', params['pixel_format']])

# GOP size (keyframe interval)
if 'gop_size' in params:
cmd_parts.extend(['-g', str(params['gop_size'])])

# B-frames
if 'b_frames' in params:
cmd_parts.extend(['-bf', str(params['b_frames'])])

# Audio sample rate
if 'audio_sample_rate' in params:
cmd_parts.extend(['-ar', str(params['audio_sample_rate'])])

# Audio channels
if 'audio_channels' in params:
cmd_parts.extend(['-ac', str(params['audio_channels'])])

# Faststart for web streaming (move moov atom to beginning)
if params.get('faststart', True):
cmd_parts.extend(['-movflags', '+faststart'])

return cmd_parts

def _handle_trim(self, params: Dict[str, Any]) -> List[str]:
"""Handle video trimming."""
cmd_parts = []

if 'start_time' in params:
cmd_parts.extend(['-ss', str(params['start_time'])])

# Support both 'start'/'start_time' naming conventions
start = params.get('start') or params.get('start_time')
if start is not None:
cmd_parts.extend(['-ss', str(start)])

# Support both 'duration' and 'end'/'end_time'
if 'duration' in params:
cmd_parts.extend(['-t', str(params['duration'])])
elif 'end_time' in params:
cmd_parts.extend(['-to', str(params['end_time'])])

else:
end = params.get('end') or params.get('end_time')
if end is not None:
cmd_parts.extend(['-to', str(end)])

return cmd_parts

def _handle_watermark(self, params: Dict[str, Any]) -> str:
Expand Down Expand Up @@ -1031,18 +1078,27 @@ async def get_file_duration(self, file_path: str) -> float:

def validate_operations(self, operations: List[Dict[str, Any]]) -> bool:
"""Validate operations before processing."""
valid_operations = {'transcode', 'trim', 'watermark', 'filter', 'stream_map', 'streaming'}

valid_operations = {
'transcode', 'trim', 'watermark', 'filter', 'stream_map', 'streaming', 'stream',
'scale', 'crop', 'rotate', 'flip', 'audio', 'subtitle', 'concat', 'thumbnail'
}

if not operations:
return True # Empty operations list is valid

for operation in operations:
if 'type' not in operation:
return False
if operation['type'] not in valid_operations:
return False

# Additional validation per operation type
# Support both flat params and nested 'params' structure
if operation['type'] == 'trim':
params = operation.get('params', {})
if 'start_time' not in params and 'duration' not in params and 'end_time' not in params:
if not params:
params = {k: v for k, v in operation.items() if k != 'type'}
if 'start' not in params and 'start_time' not in params and 'duration' not in params and 'end' not in params and 'end_time' not in params:
return False

return True