@@ -43,6 +43,46 @@ def _get_devices():
4343 )
4444
4545
46+ # On CPU we encode with yuv444p (near-lossless with crf=0) so we can compare
47+ # decoded frames directly against the source.
48+ # On CUDA we encode with yuv420p instead, because yuv444p would trigger a CPU
49+ # fallback. Since yuv420p is lossy on random data we can't compare against
50+ # source, so we compare CUDA-decoded output against CPU-decoded output instead.
51+ def _make_decoder_and_ref (tmp_path , device ):
52+ """Returns (decoder, ref_decoder_or_none, source_frames_or_none).
53+
54+ On CPU: returns (cpu_decoder, None, source_frames)
55+ On CUDA: returns (cuda_decoder, cpu_decoder, None)
56+ """
57+ if device == "cpu" :
58+ path , source_frames = _make_video_file (tmp_path , pixel_format = "yuv444p" )
59+ return VideoDecoder (path , device = "cpu" ), None , source_frames
60+ else :
61+ path , _ = _make_video_file (tmp_path , pixel_format = "yuv420p" )
62+ return (
63+ VideoDecoder (path , device = "cuda" ),
64+ VideoDecoder (path , device = "cpu" ),
65+ None ,
66+ )
67+
68+
69+ def _assert_frames_close (decoded , * , ref_decoded = None , source = None , device ):
70+ """Assert decoded frames are close to reference.
71+
72+ On CPU, compares against source frames (near-lossless yuv444p roundtrip).
73+ On CUDA, compares against CPU-decoded frames (both from same yuv420p file).
74+ """
75+ actual = decoded .cpu () if device != "cpu" else decoded
76+ if device == "cpu" :
77+ assert source is not None
78+ torch .testing .assert_close (actual , source , atol = 2 , rtol = 0 )
79+ else :
80+ assert ref_decoded is not None
81+ assert_tensor_close_on_at_least (
82+ actual , ref_decoded .cpu (), percentage = 95 , atol = 3
83+ )
84+
85+
4686class TestVideoDecoder :
4787 @pytest .mark .parametrize ("device" , _get_devices ())
4888 def test_basics (self , tmp_path , device ):
@@ -55,51 +95,34 @@ def test_basics(self, tmp_path, device):
5595
5696 @pytest .mark .parametrize ("device" , _get_devices ())
5797 def test_get_frame_at (self , tmp_path , device ):
58- if device == "cpu" :
59- path , source_frames = _make_video_file (tmp_path , pixel_format = "yuv444p" )
60- decoder = VideoDecoder (path , device = device )
61- frame = decoder .get_frame_at (0 )
62- assert isinstance (frame , Frame )
63- assert frame .data .shape == (3 , HEIGHT , WIDTH )
64- assert frame .data .dtype == torch .uint8
65- torch .testing .assert_close (frame .data , source_frames [0 ], atol = 2 , rtol = 0 )
66- else :
67- path , _ = _make_video_file (tmp_path , pixel_format = "yuv420p" )
68- cpu_decoder = VideoDecoder (path , device = "cpu" )
69- cuda_decoder = VideoDecoder (path , device = "cuda" )
70- cpu_frame = cpu_decoder .get_frame_at (0 ).data
71- cuda_frame = cuda_decoder .get_frame_at (0 ).data .cpu ()
72- assert isinstance (cuda_decoder .get_frame_at (0 ), Frame )
73- assert cuda_frame .shape == (3 , HEIGHT , WIDTH )
74- assert cuda_frame .dtype == torch .uint8
75- assert_tensor_close_on_at_least (
76- cuda_frame , cpu_frame , percentage = 95 , atol = 3
77- )
98+ decoder , ref_decoder , source_frames = _make_decoder_and_ref (tmp_path , device )
99+ frame = decoder .get_frame_at (0 )
100+ assert isinstance (frame , Frame )
101+ assert frame .data .shape == (3 , HEIGHT , WIDTH )
102+ assert frame .data .dtype == torch .uint8
103+ _assert_frames_close (
104+ frame .data ,
105+ ref_decoded = ref_decoder .get_frame_at (0 ).data if ref_decoder else None ,
106+ source = source_frames [0 ] if source_frames is not None else None ,
107+ device = device ,
108+ )
78109
79110 @pytest .mark .parametrize ("device" , _get_devices ())
80111 def test_get_frames_in_range (self , tmp_path , device ):
81- if device == "cpu" :
82- path , source_frames = _make_video_file (tmp_path , pixel_format = "yuv444p" )
83- decoder = VideoDecoder (path , device = device )
84- batch = decoder .get_frames_in_range (start = 0 , stop = 5 )
85- assert isinstance (batch , FrameBatch )
86- assert batch .data .shape == (5 , 3 , HEIGHT , WIDTH )
87- torch .testing .assert_close (
88- batch .data , source_frames [:5 ], atol = 2 , rtol = 0
89- )
90- else :
91- path , _ = _make_video_file (tmp_path , pixel_format = "yuv420p" )
92- cpu_decoder = VideoDecoder (path , device = "cpu" )
93- cuda_decoder = VideoDecoder (path , device = "cuda" )
94- cpu_batch = cpu_decoder .get_frames_in_range (start = 0 , stop = 5 ).data
95- cuda_batch = cuda_decoder .get_frames_in_range (start = 0 , stop = 5 ).data .cpu ()
96- assert isinstance (
97- cuda_decoder .get_frames_in_range (start = 0 , stop = 5 ), FrameBatch
98- )
99- assert cuda_batch .shape == (5 , 3 , HEIGHT , WIDTH )
100- assert_tensor_close_on_at_least (
101- cuda_batch , cpu_batch , percentage = 95 , atol = 3
102- )
112+ decoder , ref_decoder , source_frames = _make_decoder_and_ref (tmp_path , device )
113+ batch = decoder .get_frames_in_range (start = 0 , stop = 5 )
114+ assert isinstance (batch , FrameBatch )
115+ assert batch .data .shape == (5 , 3 , HEIGHT , WIDTH )
116+ _assert_frames_close (
117+ batch .data ,
118+ ref_decoded = (
119+ ref_decoder .get_frames_in_range (start = 0 , stop = 5 ).data
120+ if ref_decoder
121+ else None
122+ ),
123+ source = source_frames [:5 ] if source_frames is not None else None ,
124+ device = device ,
125+ )
103126
104127 @pytest .mark .parametrize ("device" , _get_devices ())
105128 def test_get_frame_played_at (self , tmp_path , device ):
@@ -112,54 +135,37 @@ def test_get_frame_played_at(self, tmp_path, device):
112135
113136 @pytest .mark .parametrize ("device" , _get_devices ())
114137 def test_getitem (self , tmp_path , device ):
115- if device == "cpu" :
116- path , source_frames = _make_video_file (tmp_path , pixel_format = "yuv444p" )
117- decoder = VideoDecoder (path , device = device )
118- tensor = decoder [0 ]
119- assert tensor .shape == (3 , HEIGHT , WIDTH )
120- torch .testing .assert_close (tensor , source_frames [0 ], atol = 2 , rtol = 0 )
121- tensors = decoder [2 :5 ]
122- assert tensors .shape == (3 , 3 , HEIGHT , WIDTH )
123- torch .testing .assert_close (
124- tensors , source_frames [2 :5 ], atol = 2 , rtol = 0
125- )
126- else :
127- path , _ = _make_video_file (tmp_path , pixel_format = "yuv420p" )
128- cpu_decoder = VideoDecoder (path , device = "cpu" )
129- cuda_decoder = VideoDecoder (path , device = "cuda" )
130- cpu_tensor = cpu_decoder [0 ]
131- cuda_tensor = cuda_decoder [0 ].cpu ()
132- assert cuda_tensor .shape == (3 , HEIGHT , WIDTH )
133- assert_tensor_close_on_at_least (
134- cuda_tensor , cpu_tensor , percentage = 95 , atol = 3
135- )
136- cpu_tensors = cpu_decoder [2 :5 ]
137- cuda_tensors = cuda_decoder [2 :5 ].cpu ()
138- assert cuda_tensors .shape == (3 , 3 , HEIGHT , WIDTH )
139- assert_tensor_close_on_at_least (
140- cuda_tensors , cpu_tensors , percentage = 95 , atol = 3
141- )
138+ decoder , ref_decoder , source_frames = _make_decoder_and_ref (tmp_path , device )
139+
140+ tensor = decoder [0 ]
141+ assert tensor .shape == (3 , HEIGHT , WIDTH )
142+ _assert_frames_close (
143+ tensor ,
144+ ref_decoded = ref_decoder [0 ] if ref_decoder else None ,
145+ source = source_frames [0 ] if source_frames is not None else None ,
146+ device = device ,
147+ )
148+
149+ tensors = decoder [2 :5 ]
150+ assert tensors .shape == (3 , 3 , HEIGHT , WIDTH )
151+ _assert_frames_close (
152+ tensors ,
153+ ref_decoded = ref_decoder [2 :5 ] if ref_decoder else None ,
154+ source = source_frames [2 :5 ] if source_frames is not None else None ,
155+ device = device ,
156+ )
142157
143158 @pytest .mark .parametrize ("device" , _get_devices ())
144159 def test_get_all_frames (self , tmp_path , device ):
145- if device == "cpu" :
146- path , source_frames = _make_video_file (tmp_path , pixel_format = "yuv444p" )
147- decoder = VideoDecoder (path , device = device )
148- all_frames = decoder .get_all_frames ()
149- assert all_frames .data .shape == (NUM_FRAMES , 3 , HEIGHT , WIDTH )
150- torch .testing .assert_close (
151- all_frames .data , source_frames , atol = 2 , rtol = 0
152- )
153- else :
154- path , _ = _make_video_file (tmp_path , pixel_format = "yuv420p" )
155- cpu_decoder = VideoDecoder (path , device = "cpu" )
156- cuda_decoder = VideoDecoder (path , device = "cuda" )
157- cpu_all = cpu_decoder .get_all_frames ().data
158- cuda_all = cuda_decoder .get_all_frames ().data .cpu ()
159- assert cuda_all .shape == (NUM_FRAMES , 3 , HEIGHT , WIDTH )
160- assert_tensor_close_on_at_least (
161- cuda_all , cpu_all , percentage = 95 , atol = 3
162- )
160+ decoder , ref_decoder , source_frames = _make_decoder_and_ref (tmp_path , device )
161+ all_frames = decoder .get_all_frames ()
162+ assert all_frames .data .shape == (NUM_FRAMES , 3 , HEIGHT , WIDTH )
163+ _assert_frames_close (
164+ all_frames .data ,
165+ ref_decoded = (ref_decoder .get_all_frames ().data if ref_decoder else None ),
166+ source = source_frames ,
167+ device = device ,
168+ )
163169
164170 @pytest .mark .parametrize ("device" , _get_devices ())
165171 def test_iteration (self , tmp_path , device ):
0 commit comments