@@ -2761,6 +2761,277 @@ def test_add_audio_output_sample_rate(self, tmp_path, method):
27612761 decoded_2 = AudioDecoder (source , stream_index = 2 ).get_all_samples ()
27622762 assert decoded_2 .sample_rate == 44_100
27632763
2764+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2765+ def test_pixel_format_errors (self , method , tmp_path ):
2766+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "mp4" )
2767+ enc .add_video (
2768+ height = 64 ,
2769+ width = 64 ,
2770+ frame_rate = 30.0 ,
2771+ pixel_format = "invalid_pix_fmt" ,
2772+ )
2773+ with pytest .raises (
2774+ RuntimeError ,
2775+ match = r"Unknown pixel format: invalid_pix_fmt[\s\S]*Supported pixel formats.*yuv420p" ,
2776+ ):
2777+ enc .open (** open_kwargs )
2778+
2779+ enc2 , _ , open_kwargs2 = self ._create_encoder (method , tmp_path , "mp4" )
2780+ enc2 .add_video (
2781+ height = 64 ,
2782+ width = 64 ,
2783+ frame_rate = 30.0 ,
2784+ pixel_format = "rgb24" ,
2785+ )
2786+ with pytest .raises (
2787+ RuntimeError ,
2788+ match = r"Specified pixel format rgb24 is not supported[\s\S]*Supported pixel formats.*yuv420p" ,
2789+ ):
2790+ enc2 .open (** open_kwargs2 )
2791+
2792+ @pytest .mark .needs_cuda
2793+ @pytest .mark .skipif (in_fbcode (), reason = "NVENC not available in fbcode" )
2794+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2795+ def test_pixel_format_gpu_override_errors (self , method , tmp_path ):
2796+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "mp4" )
2797+ enc .add_video (
2798+ height = 64 ,
2799+ width = 64 ,
2800+ frame_rate = 30.0 ,
2801+ device = "cuda" ,
2802+ pixel_format = "yuv444p" ,
2803+ )
2804+ with pytest .raises (
2805+ RuntimeError ,
2806+ match = "Video encoding on GPU currently only supports the nv12 pixel format" ,
2807+ ):
2808+ enc .open (** open_kwargs )
2809+
2810+ @pytest .mark .parametrize (
2811+ "extra_options,error" ,
2812+ [
2813+ ({"qp" : - 10 }, "qp=-10 is out of valid range" ),
2814+ ({"qp" : "" }, "Option qp expects a numeric value but got" ),
2815+ (
2816+ {"direct-pred" : "a" },
2817+ "Option direct-pred expects a numeric value but got 'a'" ,
2818+ ),
2819+ ({"tune" : "not_a_real_tune" }, "avcodec_open2 failed: Invalid argument" ),
2820+ ],
2821+ )
2822+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2823+ def test_extra_options_errors (self , method , tmp_path , extra_options , error ):
2824+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "mp4" )
2825+ enc .add_video (
2826+ height = 64 ,
2827+ width = 64 ,
2828+ frame_rate = 30.0 ,
2829+ extra_options = extra_options ,
2830+ )
2831+ with pytest .raises (RuntimeError , match = error ):
2832+ enc .open (** open_kwargs )
2833+
2834+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2835+ def test_write_frames_wrong_dtype_errors (self , method , tmp_path ):
2836+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "mp4" )
2837+ video = enc .add_video (height = 64 , width = 64 , frame_rate = 30.0 )
2838+ enc .open (** open_kwargs )
2839+ float_frames = torch .rand (2 , 3 , 64 , 64 , dtype = torch .float32 )
2840+ with pytest .raises (RuntimeError , match = "must have uint8 dtype" ):
2841+ video .write (float_frames )
2842+
2843+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2844+ def test_write_frames_wrong_ndim_errors (self , method , tmp_path ):
2845+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "mp4" )
2846+ video = enc .add_video (height = 64 , width = 64 , frame_rate = 30.0 )
2847+ enc .open (** open_kwargs )
2848+ frames_1d = torch .randint (0 , 256 , (100 ,), dtype = torch .uint8 )
2849+ with pytest .raises (RuntimeError ):
2850+ video .write (frames_1d )
2851+
2852+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2853+ def test_write_frames_wrong_num_channels_errors (self , method , tmp_path ):
2854+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "mp4" )
2855+ video = enc .add_video (height = 64 , width = 64 , frame_rate = 30.0 )
2856+ enc .open (** open_kwargs )
2857+ frames_2ch = torch .randint (0 , 256 , (2 , 2 , 64 , 64 ), dtype = torch .uint8 )
2858+ with pytest .raises (RuntimeError ):
2859+ video .write (frames_2ch )
2860+
2861+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2862+ def test_add_audio_invalid_sample_rate_errors (self , method , tmp_path ):
2863+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "wav" )
2864+ with pytest .raises (RuntimeError , match = "sample_rate must be > 0" ):
2865+ enc .add_audio (sample_rate = 0 , num_channels = 1 )
2866+
2867+ enc2 , _ , open_kwargs2 = self ._create_encoder (method , tmp_path , "wav" )
2868+ with pytest .raises (RuntimeError , match = "sample_rate must be > 0" ):
2869+ enc2 .add_audio (sample_rate = - 1 , num_channels = 1 )
2870+
2871+ @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
2872+ def test_add_audio_invalid_num_channels_errors (self , method , tmp_path ):
2873+ enc , _ , open_kwargs = self ._create_encoder (method , tmp_path , "wav" )
2874+ with pytest .raises (RuntimeError , match = "num_channels must be > 0" ):
2875+ enc .add_audio (sample_rate = 44100 , num_channels = 0 )
2876+
2877+ def test_to_file_like_custom_file_object (self , tmp_path ):
2878+ class CustomFileObject :
2879+ def __init__ (self ):
2880+ self ._file = io .BytesIO ()
2881+
2882+ def write (self , data ):
2883+ return self ._file .write (data )
2884+
2885+ def seek (self , offset , whence = 0 ):
2886+ return self ._file .seek (offset , whence )
2887+
2888+ def get_encoded_data (self ):
2889+ return self ._file .getvalue ()
2890+
2891+ source_decoder = VideoDecoder (str (TEST_SRC_2_720P .path ))
2892+ source_frames = source_decoder .get_frames_in_range (start = 0 , stop = 10 ).data
2893+ frame_rate = source_decoder .metadata .average_fps
2894+
2895+ enc = StreamingEncoder ()
2896+ file_like = CustomFileObject ()
2897+ video = enc .add_video (
2898+ height = source_frames .shape [2 ],
2899+ width = source_frames .shape [3 ],
2900+ frame_rate = frame_rate ,
2901+ pixel_format = "yuv444p" ,
2902+ crf = 0 ,
2903+ )
2904+ enc .open (file_like , format = "mp4" )
2905+ video .write (source_frames )
2906+ enc .close ()
2907+
2908+ decoded_frames = (
2909+ VideoDecoder (file_like .get_encoded_data ())
2910+ .get_frames_in_range (start = 0 , stop = 10 )
2911+ .data
2912+ )
2913+ assert_tensor_close_on_at_least (
2914+ decoded_frames , source_frames , percentage = 99 , atol = 2
2915+ )
2916+
2917+ def test_to_file_like_custom_file_object_audio (self , tmp_path ):
2918+ class CustomFileObject :
2919+ def __init__ (self ):
2920+ self ._file = io .BytesIO ()
2921+
2922+ def write (self , data ):
2923+ return self ._file .write (data )
2924+
2925+ def seek (self , offset , whence = 0 ):
2926+ return self ._file .seek (offset , whence )
2927+
2928+ def get_encoded_data (self ):
2929+ return self ._file .getvalue ()
2930+
2931+ asset = NASA_AUDIO_MP3
2932+ source_samples = AudioDecoder (str (asset .path )).get_all_samples ().data
2933+ sample_rate = asset .sample_rate
2934+ num_channels = source_samples .shape [0 ]
2935+
2936+ enc = StreamingEncoder ()
2937+ file_like = CustomFileObject ()
2938+ audio = enc .add_audio (sample_rate = sample_rate , num_channels = num_channels )
2939+ enc .open (file_like , format = "flac" )
2940+ audio .write (source_samples )
2941+ enc .close ()
2942+
2943+ decoded = AudioDecoder (file_like .get_encoded_data ()).get_all_samples ()
2944+ torch .testing .assert_close (decoded .data , source_samples , rtol = 0 , atol = 1e-4 )
2945+
2946+ def test_to_file_like_real_file_video (self , tmp_path ):
2947+ source_decoder = VideoDecoder (str (TEST_SRC_2_720P .path ))
2948+ source_frames = source_decoder .get_frames_in_range (start = 0 , stop = 10 ).data
2949+ frame_rate = source_decoder .metadata .average_fps
2950+
2951+ file_path = tmp_path / "test_real_file.mp4"
2952+ enc = StreamingEncoder ()
2953+ video = enc .add_video (
2954+ height = source_frames .shape [2 ],
2955+ width = source_frames .shape [3 ],
2956+ frame_rate = frame_rate ,
2957+ pixel_format = "yuv444p" ,
2958+ crf = 0 ,
2959+ )
2960+ with open (file_path , "wb" ) as f :
2961+ enc .open (f , format = "mp4" )
2962+ video .write (source_frames )
2963+ enc .close ()
2964+
2965+ decoded_frames = (
2966+ VideoDecoder (str (file_path )).get_frames_in_range (start = 0 , stop = 10 ).data
2967+ )
2968+ assert_tensor_close_on_at_least (
2969+ decoded_frames , source_frames , percentage = 99 , atol = 2
2970+ )
2971+
2972+ def test_to_file_like_real_file_audio (self , tmp_path ):
2973+ asset = NASA_AUDIO_MP3
2974+ source_samples = AudioDecoder (str (asset .path )).get_all_samples ().data
2975+ sample_rate = asset .sample_rate
2976+ num_channels = source_samples .shape [0 ]
2977+
2978+ file_path = tmp_path / "test_real_file.flac"
2979+ enc = StreamingEncoder ()
2980+ audio = enc .add_audio (sample_rate = sample_rate , num_channels = num_channels )
2981+ with open (file_path , "wb" ) as f :
2982+ enc .open (f , format = "flac" )
2983+ audio .write (source_samples )
2984+ enc .close ()
2985+
2986+ decoded = AudioDecoder (str (file_path )).get_all_samples ()
2987+ torch .testing .assert_close (decoded .data , source_samples , rtol = 0 , atol = 1e-4 )
2988+
2989+ def test_to_file_like_bad_methods_video (self ):
2990+ class NoWriteMethod :
2991+ def seek (self , offset , whence = 0 ):
2992+ return 0
2993+
2994+ enc = StreamingEncoder ()
2995+ enc .add_video (height = 64 , width = 64 , frame_rate = 30.0 )
2996+ with pytest .raises (
2997+ RuntimeError , match = "File like object must implement a write method"
2998+ ):
2999+ enc .open (NoWriteMethod (), format = "mp4" )
3000+
3001+ class NoSeekMethod :
3002+ def write (self , data ):
3003+ return len (data )
3004+
3005+ enc2 = StreamingEncoder ()
3006+ enc2 .add_video (height = 64 , width = 64 , frame_rate = 30.0 )
3007+ with pytest .raises (
3008+ RuntimeError , match = "File like object must implement a seek method"
3009+ ):
3010+ enc2 .open (NoSeekMethod (), format = "mp4" )
3011+
3012+ def test_to_file_like_bad_methods_audio (self ):
3013+ class NoWriteMethod :
3014+ def seek (self , offset , whence = 0 ):
3015+ return 0
3016+
3017+ enc = StreamingEncoder ()
3018+ enc .add_audio (sample_rate = 44100 , num_channels = 1 )
3019+ with pytest .raises (
3020+ RuntimeError , match = "File like object must implement a write method"
3021+ ):
3022+ enc .open (NoWriteMethod (), format = "wav" )
3023+
3024+ class NoSeekMethod :
3025+ def write (self , data ):
3026+ return len (data )
3027+
3028+ enc2 = StreamingEncoder ()
3029+ enc2 .add_audio (sample_rate = 44100 , num_channels = 1 )
3030+ with pytest .raises (
3031+ RuntimeError , match = "File like object must implement a seek method"
3032+ ):
3033+ enc2 .open (NoSeekMethod (), format = "wav" )
3034+
27643035 @needs_ffmpeg_cli
27653036 @pytest .mark .parametrize ("method" , ("to_file" , "to_file_like" ))
27663037 @pytest .mark .parametrize (
0 commit comments