@@ -570,7 +570,13 @@ def write(self, data):
570570
571571class TestVideoEncoder :
572572 def decode (self , source = None ) -> torch .Tensor :
573- return VideoDecoder (source ).get_frames_in_range (start = 0 , stop = 60 )
573+ return VideoDecoder (source ).get_frames_in_range (start = 0 , stop = 60 ).data
574+
575+ def decode_and_get_frame_rate (self , source = None ):
576+ decoder = VideoDecoder (source )
577+ frames = decoder .get_frames_in_range (start = 0 , stop = 60 ).data
578+ frame_rate = decoder .metadata .average_fps
579+ return frames , frame_rate
574580
575581 def _get_video_metadata (self , file_path , fields ):
576582 """Helper function to get video metadata from a file using ffprobe."""
@@ -826,26 +832,25 @@ def test_round_trip(self, tmp_path, format, method):
826832 ffmpeg_version == 4 or (IS_WINDOWS and ffmpeg_version in (6 , 7 ))
827833 ):
828834 pytest .skip ("Codec for webm is not available in this FFmpeg installation." )
829- source_frames = self .decode (TEST_SRC_2_720P .path ). data
835+ source_frames , frame_rate = self .decode_and_get_frame_rate (TEST_SRC_2_720P .path )
830836
831- # Frame rate is fixed with num frames decoded
832- encoder = VideoEncoder (frames = source_frames , frame_rate = 30 )
837+ encoder = VideoEncoder (frames = source_frames , frame_rate = frame_rate )
833838
834839 if method == "to_file" :
835840 encoded_path = str (tmp_path / f"encoder_output.{ format } " )
836841 encoder .to_file (dest = encoded_path , pixel_format = "yuv444p" , crf = 0 )
837- round_trip_frames = self .decode (encoded_path ). data
842+ round_trip_frames = self .decode (encoded_path )
838843 elif method == "to_tensor" :
839844 encoded_tensor = encoder .to_tensor (
840845 format = format , pixel_format = "yuv444p" , crf = 0
841846 )
842- round_trip_frames = self .decode (encoded_tensor ). data
847+ round_trip_frames = self .decode (encoded_tensor )
843848 elif method == "to_file_like" :
844849 file_like = io .BytesIO ()
845850 encoder .to_file_like (
846851 file_like = file_like , format = format , pixel_format = "yuv444p" , crf = 0
847852 )
848- round_trip_frames = self .decode (file_like .getvalue ()). data
853+ round_trip_frames = self .decode (file_like .getvalue ())
849854 else :
850855 raise ValueError (f"Unknown method: { method } " )
851856
@@ -878,8 +883,8 @@ def test_against_to_file(self, tmp_path, format, method):
878883 ):
879884 pytest .skip ("Codec for webm is not available in this FFmpeg installation." )
880885
881- source_frames = self .decode (TEST_SRC_2_720P .path ). data
882- encoder = VideoEncoder (frames = source_frames , frame_rate = 30 )
886+ source_frames , frame_rate = self .decode_and_get_frame_rate (TEST_SRC_2_720P .path )
887+ encoder = VideoEncoder (frames = source_frames , frame_rate = frame_rate )
883888
884889 encoded_file = tmp_path / f"output.{ format } "
885890 encoder .to_file (dest = encoded_file , crf = 0 )
@@ -892,8 +897,8 @@ def test_against_to_file(self, tmp_path, format, method):
892897 encoded_output = file_like .getvalue ()
893898
894899 torch .testing .assert_close (
895- self .decode (encoded_file ). data ,
896- self .decode (encoded_output ). data ,
900+ self .decode (encoded_file ),
901+ self .decode (encoded_output ),
897902 atol = 0 ,
898903 rtol = 0 ,
899904 )
@@ -936,15 +941,14 @@ def test_video_encoder_against_ffmpeg_cli(
936941 if format in ("avi" , "flv" ) and pixel_format == "yuv444p" :
937942 pytest .skip (f"Default codec for { format } does not support { pixel_format } " )
938943
939- source_frames = self .decode (TEST_SRC_2_720P .path ). data
944+ source_frames , frame_rate = self .decode_and_get_frame_rate (TEST_SRC_2_720P .path )
940945
941946 # Encode with FFmpeg CLI
942947 temp_raw_path = str (tmp_path / "temp_input.raw" )
943948 with open (temp_raw_path , "wb" ) as f :
944949 f .write (source_frames .permute (0 , 2 , 3 , 1 ).cpu ().numpy ().tobytes ())
945950
946951 ffmpeg_encoded_path = str (tmp_path / f"ffmpeg_output.{ format } " )
947- frame_rate = 30
948952 # Some codecs (ex. MPEG4) do not support CRF or preset.
949953 # Flags not supported by the selected codec will be ignored.
950954 ffmpeg_cmd = [
@@ -983,15 +987,15 @@ def test_video_encoder_against_ffmpeg_cli(
983987 crf = crf ,
984988 preset = preset ,
985989 )
986- encoder_frames = self .decode (encoder_output_path ). data
990+ encoder_frames = self .decode (encoder_output_path )
987991 elif method == "to_tensor" :
988992 encoded_output = encoder .to_tensor (
989993 format = format ,
990994 pixel_format = pixel_format ,
991995 crf = crf ,
992996 preset = preset ,
993997 )
994- encoder_frames = self .decode (encoded_output ). data
998+ encoder_frames = self .decode (encoded_output )
995999 elif method == "to_file_like" :
9961000 file_like = io .BytesIO ()
9971001 encoder .to_file_like (
@@ -1001,7 +1005,7 @@ def test_video_encoder_against_ffmpeg_cli(
10011005 crf = crf ,
10021006 preset = preset ,
10031007 )
1004- encoder_frames = self .decode (file_like .getvalue ()). data
1008+ encoder_frames = self .decode (file_like .getvalue ())
10051009 else :
10061010 raise ValueError (f"Unknown method: { method } " )
10071011
@@ -1047,24 +1051,24 @@ def seek(self, offset, whence=0):
10471051 def get_encoded_data (self ):
10481052 return self ._file .getvalue ()
10491053
1050- source_frames = self .decode (TEST_SRC_2_720P .path ). data
1051- encoder = VideoEncoder (frames = source_frames , frame_rate = 30 )
1054+ source_frames , frame_rate = self .decode_and_get_frame_rate (TEST_SRC_2_720P .path )
1055+ encoder = VideoEncoder (frames = source_frames , frame_rate = frame_rate )
10521056
10531057 file_like = CustomFileObject ()
10541058 encoder .to_file_like (file_like , format = "mp4" , pixel_format = "yuv444p" , crf = 0 )
10551059 decoded_frames = self .decode (file_like .get_encoded_data ())
10561060
10571061 torch .testing .assert_close (
1058- decoded_frames . data ,
1062+ decoded_frames ,
10591063 source_frames ,
10601064 atol = 2 ,
10611065 rtol = 0 ,
10621066 )
10631067
10641068 def test_to_file_like_real_file (self , tmp_path ):
10651069 """Test to_file_like with a real file opened in binary write mode."""
1066- source_frames = self .decode (TEST_SRC_2_720P .path ). data
1067- encoder = VideoEncoder (frames = source_frames , frame_rate = 30 )
1070+ source_frames , frame_rate = self .decode_and_get_frame_rate (TEST_SRC_2_720P .path )
1071+ encoder = VideoEncoder (frames = source_frames , frame_rate = frame_rate )
10681072
10691073 file_path = tmp_path / "test_file_like.mp4"
10701074
@@ -1073,15 +1077,15 @@ def test_to_file_like_real_file(self, tmp_path):
10731077 decoded_frames = self .decode (str (file_path ))
10741078
10751079 torch .testing .assert_close (
1076- decoded_frames . data ,
1080+ decoded_frames ,
10771081 source_frames ,
10781082 atol = 2 ,
10791083 rtol = 0 ,
10801084 )
10811085
10821086 def test_to_file_like_bad_methods (self ):
1083- source_frames = self .decode (TEST_SRC_2_720P .path ). data
1084- encoder = VideoEncoder (frames = source_frames , frame_rate = 30 )
1087+ source_frames , frame_rate = self .decode_and_get_frame_rate (TEST_SRC_2_720P .path )
1088+ encoder = VideoEncoder (frames = source_frames , frame_rate = frame_rate )
10851089
10861090 class NoWriteMethod :
10871091 def seek (self , offset , whence = 0 ):
@@ -1174,8 +1178,8 @@ def test_codec_spec_vs_impl_equivalence(self, tmp_path, codec_spec, codec_impl):
11741178 == codec_spec
11751179 )
11761180
1177- frames_spec = self .decode (spec_output ). data
1178- frames_impl = self .decode (impl_output ). data
1181+ frames_spec = self .decode (spec_output )
1182+ frames_impl = self .decode (impl_output )
11791183 torch .testing .assert_close (frames_spec , frames_impl , rtol = 0 , atol = 0 )
11801184
11811185 @pytest .mark .skipif (in_fbcode (), reason = "ffprobe not available" )
@@ -1210,3 +1214,20 @@ def test_extra_options_utilized(self, tmp_path, profile, colorspace, color_range
12101214 assert metadata ["profile" ].lower () == expected_profile
12111215 assert metadata ["color_space" ] == colorspace
12121216 assert metadata ["color_range" ] == color_range
1217+
1218+ @pytest .mark .parametrize ("frame_rate" , [29.97 , 59.94 , 5.001 ])
1219+ def test_fractional_frame_rate (self , tmp_path , frame_rate ):
1220+ source_frames = torch .zeros ((10 , 3 , 64 , 64 ), dtype = torch .uint8 )
1221+ encoder = VideoEncoder (frames = source_frames , frame_rate = frame_rate )
1222+ output_path = str (tmp_path / "output.mp4" )
1223+ encoder .to_file (dest = output_path )
1224+ # Assert the encoded frame rate via file metadata
1225+ metadata = self ._get_video_metadata (output_path , fields = ["r_frame_rate" ])
1226+ num , den = metadata ["r_frame_rate" ].split ("/" )
1227+ encoded_frame_rate = int (num ) / int (den )
1228+ assert encoded_frame_rate == frame_rate
1229+ # Assert the decoded frame rate matches the input frame rate
1230+ _decoded_frames , decoded_frame_rate = self .decode_and_get_frame_rate (
1231+ output_path
1232+ )
1233+ assert decoded_frame_rate == frame_rate
0 commit comments