From 61c3fafcd727cf23b54528a8a26e71b55c35546f Mon Sep 17 00:00:00 2001 From: ptaindia Date: Mon, 8 Dec 2025 01:07:59 +0530 Subject: [PATCH 1/2] Deep audit fixes: Enhanced FFmpeg transcoding capabilities ## FFmpegWrapper fixes: - Updated validate_operations with all new operation types - Fixed trim handler to support both 'start'/'start_time' naming - Allow empty operations list ## Enhanced transcode operation: - Added profile support (baseline, main, high, high10, high422, high444) - Added pixel format support (yuv420p, yuv422p, yuv444p, 10-bit) - Added hardware acceleration preference (auto, none, nvenc, qsv, vaapi) - Added VBV buffer control (max_bitrate, buffer_size) - Added GOP size (keyframe interval) control - Added B-frames control - Added audio sample rate and channels - Added faststart flag for web streaming - Added two-pass encoding flag ## New codec support: - Video: prores, dnxhd - Audio: eac3, flac, pcm_s16le, pcm_s24le --- api/utils/validators.py | 91 +++++++++++++++++++++++++++++++----- worker/utils/ffmpeg.py | 100 +++++++++++++++++++++++++++++++--------- 2 files changed, 158 insertions(+), 33 deletions(-) diff --git a/api/utils/validators.py b/api/utils/validators.py index 052ad21..0b1b9e6 100644 --- a/api/utils/validators.py +++ b/api/utils/validators.py @@ -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"] @@ -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"] @@ -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"] @@ -667,13 +670,40 @@ 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") @@ -681,7 +711,7 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]: validated_resolution = validate_resolution(width, height) if validated_resolution: validated.update(validated_resolution) - + # Validate frame rate if "fps" in op: fps = op["fps"] @@ -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"] @@ -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 diff --git a/worker/utils/ffmpeg.py b/worker/utils/ffmpeg.py index be34eb5..af43c9f 100644 --- a/worker/utils/ffmpeg.py +++ b/worker/utils/ffmpeg.py @@ -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: @@ -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 \ No newline at end of file From febb186ee2dafaf31adc52c60e5ff047fba0fb99 Mon Sep 17 00:00:00 2001 From: ptaindia Date: Mon, 8 Dec 2025 01:09:05 +0530 Subject: [PATCH 2/2] Add data/ directory to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a187747..40052ec 100644 --- a/.gitignore +++ b/.gitignore @@ -212,3 +212,4 @@ k8s/*/secrets/ local/ dev/ development/ +data/