Skip to content

Commit 61c3faf

Browse files
committed
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
1 parent 994a198 commit 61c3faf

File tree

2 files changed

+158
-33
lines changed

2 files changed

+158
-33
lines changed

api/utils/validators.py

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -635,12 +635,15 @@ def validate_concat_operation(op: Dict[str, Any]) -> Dict[str, Any]:
635635
def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
636636
"""Validate transcode operation with enhanced security checks."""
637637
validated = {"type": "transcode"}
638-
638+
639639
# Allowed video codecs
640-
ALLOWED_VIDEO_CODECS = {'h264', 'h265', 'hevc', 'vp8', 'vp9', 'av1', 'libx264', 'libx265', 'copy'}
641-
ALLOWED_AUDIO_CODECS = {'aac', 'mp3', 'opus', 'vorbis', 'ac3', 'libfdk_aac', 'copy'}
640+
ALLOWED_VIDEO_CODECS = {'h264', 'h265', 'hevc', 'vp8', 'vp9', 'av1', 'libx264', 'libx265', 'copy', 'prores', 'dnxhd'}
641+
ALLOWED_AUDIO_CODECS = {'aac', 'mp3', 'opus', 'vorbis', 'ac3', 'eac3', 'libfdk_aac', 'flac', 'pcm_s16le', 'pcm_s24le', 'copy'}
642642
ALLOWED_PRESETS = {'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'}
643-
643+
ALLOWED_PROFILES = {'baseline', 'main', 'high', 'high10', 'high422', 'high444'}
644+
ALLOWED_PIXEL_FORMATS = {'yuv420p', 'yuv422p', 'yuv444p', 'yuv420p10le', 'yuv422p10le', 'rgb24', 'rgba'}
645+
ALLOWED_HW_ACCEL = {'auto', 'none', 'nvenc', 'qsv', 'vaapi', 'videotoolbox'}
646+
644647
# Validate video codec
645648
if "video_codec" in op:
646649
codec = op["video_codec"]
@@ -649,7 +652,7 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
649652
if codec not in ALLOWED_VIDEO_CODECS:
650653
raise ValueError(f"Invalid video codec: {codec}")
651654
validated["video_codec"] = codec
652-
655+
653656
# Validate audio codec
654657
if "audio_codec" in op:
655658
codec = op["audio_codec"]
@@ -658,7 +661,7 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
658661
if codec not in ALLOWED_AUDIO_CODECS:
659662
raise ValueError(f"Invalid audio codec: {codec}")
660663
validated["audio_codec"] = codec
661-
664+
662665
# Validate preset
663666
if "preset" in op:
664667
preset = op["preset"]
@@ -667,21 +670,48 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
667670
if preset not in ALLOWED_PRESETS:
668671
raise ValueError(f"Invalid preset: {preset}")
669672
validated["preset"] = preset
670-
673+
674+
# Validate profile (for H.264/H.265)
675+
if "profile" in op:
676+
profile = op["profile"]
677+
if not isinstance(profile, str):
678+
raise ValueError("Profile must be a string")
679+
if profile not in ALLOWED_PROFILES:
680+
raise ValueError(f"Invalid profile: {profile}")
681+
validated["profile"] = profile
682+
683+
# Validate pixel format
684+
if "pixel_format" in op or "pix_fmt" in op:
685+
pix_fmt = op.get("pixel_format") or op.get("pix_fmt")
686+
if pix_fmt not in ALLOWED_PIXEL_FORMATS:
687+
raise ValueError(f"Invalid pixel format: {pix_fmt}")
688+
validated["pixel_format"] = pix_fmt
689+
690+
# Validate hardware acceleration
691+
if "hardware_acceleration" in op or "hw_accel" in op:
692+
hw = op.get("hardware_acceleration") or op.get("hw_accel")
693+
if hw not in ALLOWED_HW_ACCEL:
694+
raise ValueError(f"Invalid hardware acceleration: {hw}")
695+
validated["hardware_acceleration"] = hw
696+
671697
# Validate bitrates
672698
if "video_bitrate" in op:
673699
validated["video_bitrate"] = validate_bitrate(op["video_bitrate"])
674700
if "audio_bitrate" in op:
675701
validated["audio_bitrate"] = validate_bitrate(op["audio_bitrate"])
676-
702+
if "max_bitrate" in op:
703+
validated["max_bitrate"] = validate_bitrate(op["max_bitrate"])
704+
if "buffer_size" in op:
705+
validated["buffer_size"] = validate_bitrate(op["buffer_size"])
706+
677707
# Validate resolution
678708
if "width" in op or "height" in op:
679709
width = op.get("width")
680710
height = op.get("height")
681711
validated_resolution = validate_resolution(width, height)
682712
if validated_resolution:
683713
validated.update(validated_resolution)
684-
714+
685715
# Validate frame rate
686716
if "fps" in op:
687717
fps = op["fps"]
@@ -691,7 +721,7 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
691721
validated["fps"] = float(fps)
692722
else:
693723
raise ValueError("FPS must be a number")
694-
724+
695725
# Validate CRF
696726
if "crf" in op:
697727
crf = op["crf"]
@@ -701,7 +731,46 @@ def validate_transcode_operation(op: Dict[str, Any]) -> Dict[str, Any]:
701731
validated["crf"] = int(crf)
702732
else:
703733
raise ValueError("CRF must be a number")
704-
734+
735+
# Validate GOP size (keyframe interval)
736+
if "gop_size" in op or "keyint" in op:
737+
gop = op.get("gop_size") or op.get("keyint")
738+
if isinstance(gop, int):
739+
if gop < 1 or gop > 600:
740+
raise ValueError("GOP size out of valid range (1-600)")
741+
validated["gop_size"] = gop
742+
else:
743+
raise ValueError("GOP size must be an integer")
744+
745+
# Validate B-frames
746+
if "b_frames" in op or "bframes" in op:
747+
bf = op.get("b_frames") or op.get("bframes")
748+
if isinstance(bf, int):
749+
if bf < 0 or bf > 16:
750+
raise ValueError("B-frames out of valid range (0-16)")
751+
validated["b_frames"] = bf
752+
else:
753+
raise ValueError("B-frames must be an integer")
754+
755+
# Validate two-pass encoding
756+
if "two_pass" in op:
757+
validated["two_pass"] = bool(op["two_pass"])
758+
759+
# Validate audio sample rate
760+
if "audio_sample_rate" in op:
761+
sr = op["audio_sample_rate"]
762+
allowed_rates = [8000, 11025, 16000, 22050, 32000, 44100, 48000, 96000]
763+
if sr not in allowed_rates:
764+
raise ValueError(f"Invalid audio sample rate: {sr}")
765+
validated["audio_sample_rate"] = sr
766+
767+
# Validate audio channels
768+
if "audio_channels" in op:
769+
channels = op["audio_channels"]
770+
if channels not in [1, 2, 6, 8]:
771+
raise ValueError("Audio channels must be 1, 2, 6, or 8")
772+
validated["audio_channels"] = channels
773+
705774
return validated
706775

707776

worker/utils/ffmpeg.py

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -518,50 +518,97 @@ def _validate_time_string(self, time_str: str, param_name: str):
518518
def _handle_transcode(self, params: Dict[str, Any]) -> List[str]:
519519
"""Handle video transcoding parameters."""
520520
cmd_parts = []
521-
521+
522+
# Hardware acceleration preference
523+
hw_pref = params.get('hardware_acceleration', 'auto')
524+
522525
# Video codec
523526
if 'video_codec' in params:
524527
codec = params['video_codec']
525-
encoder = HardwareAcceleration.get_best_encoder(codec, self.hardware_caps)
528+
if hw_pref == 'none' or codec == 'copy':
529+
# Use software encoder or copy
530+
encoder = 'copy' if codec == 'copy' else f"lib{codec}" if codec in ('x264', 'x265') else codec
531+
else:
532+
encoder = HardwareAcceleration.get_best_encoder(codec, self.hardware_caps)
526533
cmd_parts.extend(['-c:v', encoder])
527-
534+
528535
# Audio codec
529536
if 'audio_codec' in params:
530537
cmd_parts.extend(['-c:a', params['audio_codec']])
531-
532-
# Bitrate
538+
539+
# Video bitrate with VBV buffer
533540
if 'video_bitrate' in params:
534-
cmd_parts.extend(['-b:v', params['video_bitrate']])
541+
cmd_parts.extend(['-b:v', str(params['video_bitrate'])])
542+
if 'max_bitrate' in params:
543+
cmd_parts.extend(['-maxrate', str(params['max_bitrate'])])
544+
if 'buffer_size' in params:
545+
cmd_parts.extend(['-bufsize', str(params['buffer_size'])])
546+
547+
# Audio bitrate
535548
if 'audio_bitrate' in params:
536-
cmd_parts.extend(['-b:a', params['audio_bitrate']])
537-
549+
cmd_parts.extend(['-b:a', str(params['audio_bitrate'])])
550+
538551
# Resolution
539552
if 'width' in params and 'height' in params:
540553
cmd_parts.extend(['-s', f"{params['width']}x{params['height']}"])
541-
554+
542555
# Frame rate
543556
if 'fps' in params:
544557
cmd_parts.extend(['-r', str(params['fps'])])
545-
558+
546559
# Quality settings
547560
if 'crf' in params:
548561
cmd_parts.extend(['-crf', str(params['crf'])])
549562
if 'preset' in params:
550563
cmd_parts.extend(['-preset', params['preset']])
551-
564+
565+
# Profile (H.264/H.265)
566+
if 'profile' in params:
567+
cmd_parts.extend(['-profile:v', params['profile']])
568+
569+
# Pixel format
570+
if 'pixel_format' in params:
571+
cmd_parts.extend(['-pix_fmt', params['pixel_format']])
572+
573+
# GOP size (keyframe interval)
574+
if 'gop_size' in params:
575+
cmd_parts.extend(['-g', str(params['gop_size'])])
576+
577+
# B-frames
578+
if 'b_frames' in params:
579+
cmd_parts.extend(['-bf', str(params['b_frames'])])
580+
581+
# Audio sample rate
582+
if 'audio_sample_rate' in params:
583+
cmd_parts.extend(['-ar', str(params['audio_sample_rate'])])
584+
585+
# Audio channels
586+
if 'audio_channels' in params:
587+
cmd_parts.extend(['-ac', str(params['audio_channels'])])
588+
589+
# Faststart for web streaming (move moov atom to beginning)
590+
if params.get('faststart', True):
591+
cmd_parts.extend(['-movflags', '+faststart'])
592+
552593
return cmd_parts
553594

554595
def _handle_trim(self, params: Dict[str, Any]) -> List[str]:
555596
"""Handle video trimming."""
556597
cmd_parts = []
557-
558-
if 'start_time' in params:
559-
cmd_parts.extend(['-ss', str(params['start_time'])])
598+
599+
# Support both 'start'/'start_time' naming conventions
600+
start = params.get('start') or params.get('start_time')
601+
if start is not None:
602+
cmd_parts.extend(['-ss', str(start)])
603+
604+
# Support both 'duration' and 'end'/'end_time'
560605
if 'duration' in params:
561606
cmd_parts.extend(['-t', str(params['duration'])])
562-
elif 'end_time' in params:
563-
cmd_parts.extend(['-to', str(params['end_time'])])
564-
607+
else:
608+
end = params.get('end') or params.get('end_time')
609+
if end is not None:
610+
cmd_parts.extend(['-to', str(end)])
611+
565612
return cmd_parts
566613

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

10321079
def validate_operations(self, operations: List[Dict[str, Any]]) -> bool:
10331080
"""Validate operations before processing."""
1034-
valid_operations = {'transcode', 'trim', 'watermark', 'filter', 'stream_map', 'streaming'}
1035-
1081+
valid_operations = {
1082+
'transcode', 'trim', 'watermark', 'filter', 'stream_map', 'streaming', 'stream',
1083+
'scale', 'crop', 'rotate', 'flip', 'audio', 'subtitle', 'concat', 'thumbnail'
1084+
}
1085+
1086+
if not operations:
1087+
return True # Empty operations list is valid
1088+
10361089
for operation in operations:
10371090
if 'type' not in operation:
10381091
return False
10391092
if operation['type'] not in valid_operations:
10401093
return False
1041-
1094+
10421095
# Additional validation per operation type
1096+
# Support both flat params and nested 'params' structure
10431097
if operation['type'] == 'trim':
10441098
params = operation.get('params', {})
1045-
if 'start_time' not in params and 'duration' not in params and 'end_time' not in params:
1099+
if not params:
1100+
params = {k: v for k, v in operation.items() if k != 'type'}
1101+
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:
10461102
return False
1047-
1103+
10481104
return True

0 commit comments

Comments
 (0)