Skip to content

Commit cd1fee0

Browse files
committed
Add smoke tests
1 parent ee03112 commit cd1fee0

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

test/smoke_test.py

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
import torch
5+
6+
from torchcodec._frame import AudioSamples, Frame, FrameBatch
7+
from torchcodec.decoders import AudioDecoder, VideoDecoder
8+
from torchcodec.encoders import AudioEncoder, VideoEncoder
9+
from torchcodec.encoders._multi_stream_encoder import StreamingEncoder
10+
11+
12+
NUM_FRAMES = 10
13+
HEIGHT = 64
14+
WIDTH = 32
15+
FRAME_RATE = 30
16+
NUM_AUDIO_CHANNELS = 2
17+
SAMPLE_RATE = 16_000
18+
NUM_SAMPLES = 10_000
19+
20+
21+
def _make_video_file(tmp_path, **encoder_kwargs):
22+
frames = torch.randint(0, 256, (NUM_FRAMES, 3, HEIGHT, WIDTH), dtype=torch.uint8)
23+
path = tmp_path / "test.mp4"
24+
VideoEncoder(frames, frame_rate=FRAME_RATE).to_file(
25+
path, pixel_format="yuv444p", crf=0, **encoder_kwargs
26+
)
27+
return path, frames
28+
29+
30+
def _make_audio_file(tmp_path, *, format="wav"):
31+
samples = torch.rand(NUM_AUDIO_CHANNELS, NUM_SAMPLES) * 2 - 1
32+
path = tmp_path / f"test.{format}"
33+
AudioEncoder(samples, sample_rate=SAMPLE_RATE).to_file(path)
34+
return path, samples
35+
36+
37+
def _get_devices():
38+
return (
39+
"cpu",
40+
pytest.param("cuda", marks=pytest.mark.needs_cuda),
41+
)
42+
43+
44+
class TestVideoDecoder:
45+
@pytest.mark.parametrize("device", _get_devices())
46+
def test_basics(self, tmp_path, device):
47+
path, source_frames = _make_video_file(tmp_path)
48+
decoder = VideoDecoder(path, device=device)
49+
50+
assert len(decoder) == NUM_FRAMES
51+
assert decoder.metadata.height == HEIGHT
52+
assert decoder.metadata.width == WIDTH
53+
54+
@pytest.mark.parametrize("device", _get_devices())
55+
def test_get_frame_at(self, tmp_path, device):
56+
path, source_frames = _make_video_file(tmp_path)
57+
decoder = VideoDecoder(path, device=device)
58+
59+
frame = decoder.get_frame_at(0)
60+
assert isinstance(frame, Frame)
61+
assert frame.data.shape == (3, HEIGHT, WIDTH)
62+
assert frame.data.dtype == torch.uint8
63+
torch.testing.assert_close(frame.data.cpu(), source_frames[0], atol=5, rtol=0)
64+
65+
@pytest.mark.parametrize("device", _get_devices())
66+
def test_get_frames_in_range(self, tmp_path, device):
67+
path, source_frames = _make_video_file(tmp_path)
68+
decoder = VideoDecoder(path, device=device)
69+
70+
batch = decoder.get_frames_in_range(start=0, stop=5)
71+
assert isinstance(batch, FrameBatch)
72+
assert batch.data.shape == (5, 3, HEIGHT, WIDTH)
73+
torch.testing.assert_close(batch.data.cpu(), source_frames[:5], atol=5, rtol=0)
74+
75+
@pytest.mark.parametrize("device", _get_devices())
76+
def test_get_frame_played_at(self, tmp_path, device):
77+
path, _ = _make_video_file(tmp_path)
78+
decoder = VideoDecoder(path, device=device)
79+
80+
frame = decoder.get_frame_played_at(0.0)
81+
assert isinstance(frame, Frame)
82+
assert frame.data.shape == (3, HEIGHT, WIDTH)
83+
84+
@pytest.mark.parametrize("device", _get_devices())
85+
def test_getitem(self, tmp_path, device):
86+
path, source_frames = _make_video_file(tmp_path)
87+
decoder = VideoDecoder(path, device=device)
88+
89+
tensor = decoder[0]
90+
assert tensor.shape == (3, HEIGHT, WIDTH)
91+
torch.testing.assert_close(tensor.cpu(), source_frames[0], atol=5, rtol=0)
92+
93+
tensors = decoder[2:5]
94+
assert tensors.shape == (3, 3, HEIGHT, WIDTH)
95+
torch.testing.assert_close(tensors.cpu(), source_frames[2:5], atol=5, rtol=0)
96+
97+
@pytest.mark.parametrize("device", _get_devices())
98+
def test_get_all_frames(self, tmp_path, device):
99+
path, source_frames = _make_video_file(tmp_path)
100+
decoder = VideoDecoder(path, device=device)
101+
102+
all_frames = decoder.get_all_frames()
103+
assert all_frames.data.shape == (NUM_FRAMES, 3, HEIGHT, WIDTH)
104+
torch.testing.assert_close(all_frames.data.cpu(), source_frames, atol=5, rtol=0)
105+
106+
@pytest.mark.parametrize("device", _get_devices())
107+
def test_iteration(self, tmp_path, device):
108+
path, _ = _make_video_file(tmp_path)
109+
decoder = VideoDecoder(path, device=device)
110+
111+
count = 0
112+
for frame in decoder:
113+
assert frame.shape == (3, HEIGHT, WIDTH)
114+
count += 1
115+
assert count == NUM_FRAMES
116+
117+
118+
class TestAudioDecoder:
119+
def test_basics(self, tmp_path):
120+
path, source_samples = _make_audio_file(tmp_path)
121+
decoder = AudioDecoder(path)
122+
123+
assert decoder.metadata.sample_rate == SAMPLE_RATE
124+
assert decoder.metadata.num_channels == NUM_AUDIO_CHANNELS
125+
126+
def test_get_all_samples(self, tmp_path):
127+
path, source_samples = _make_audio_file(tmp_path)
128+
decoder = AudioDecoder(path)
129+
130+
samples = decoder.get_all_samples()
131+
assert isinstance(samples, AudioSamples)
132+
assert samples.data.shape == (NUM_AUDIO_CHANNELS, NUM_SAMPLES)
133+
assert samples.sample_rate == SAMPLE_RATE
134+
assert samples.pts_seconds == 0.0
135+
assert samples.duration_seconds > 0
136+
torch.testing.assert_close(samples.data, source_samples, atol=1e-4, rtol=1e-3)
137+
138+
def test_get_samples_played_in_range(self, tmp_path):
139+
path, source_samples = _make_audio_file(tmp_path)
140+
decoder = AudioDecoder(path)
141+
142+
samples = decoder.get_samples_played_in_range(
143+
start_seconds=0.0, stop_seconds=0.1
144+
)
145+
assert isinstance(samples, AudioSamples)
146+
assert samples.data.shape[0] == NUM_AUDIO_CHANNELS
147+
expected_num_samples = int(0.1 * SAMPLE_RATE)
148+
assert abs(samples.data.shape[1] - expected_num_samples) <= 1
149+
150+
def test_resample_on_decode(self, tmp_path):
151+
path, source_samples = _make_audio_file(tmp_path)
152+
153+
target_sr = 8000
154+
decoder = AudioDecoder(path, sample_rate=target_sr, num_channels=1)
155+
156+
samples = decoder.get_all_samples()
157+
assert samples.sample_rate == target_sr
158+
assert samples.data.shape[0] == 1
159+
160+
161+
class TestVideoEncoder:
162+
def test_to_file(self, tmp_path):
163+
frames = torch.randint(0, 256, (5, 3, HEIGHT, WIDTH), dtype=torch.uint8)
164+
path = str(tmp_path / "out.mp4")
165+
VideoEncoder(frames, frame_rate=FRAME_RATE).to_file(path)
166+
assert Path(path).stat().st_size > 0
167+
168+
decoder = VideoDecoder(path)
169+
assert len(decoder) == 5
170+
171+
def test_to_tensor(self):
172+
frames = torch.randint(0, 256, (5, 3, HEIGHT, WIDTH), dtype=torch.uint8)
173+
encoded = VideoEncoder(frames, frame_rate=FRAME_RATE).to_tensor(format="mp4")
174+
assert encoded.dtype == torch.uint8
175+
assert encoded.ndim == 1
176+
assert len(encoded) > 0
177+
178+
def test_roundtrip_lossless(self, tmp_path):
179+
frames = torch.randint(0, 256, (5, 3, HEIGHT, WIDTH), dtype=torch.uint8)
180+
path = str(tmp_path / "lossless.mp4")
181+
VideoEncoder(frames, frame_rate=FRAME_RATE).to_file(
182+
path, pixel_format="yuv444p", crf=0
183+
)
184+
decoder = VideoDecoder(path)
185+
decoded = decoder.get_all_frames()
186+
torch.testing.assert_close(decoded.data, frames, atol=2, rtol=0)
187+
188+
189+
class TestAudioEncoder:
190+
def test_to_file_wav(self, tmp_path):
191+
samples = torch.rand(2, NUM_SAMPLES) * 2 - 1
192+
path = str(tmp_path / "out.wav")
193+
AudioEncoder(samples, sample_rate=SAMPLE_RATE).to_file(path)
194+
assert Path(path).stat().st_size > 0
195+
196+
decoder = AudioDecoder(path)
197+
assert decoder.metadata.sample_rate == SAMPLE_RATE
198+
assert decoder.metadata.num_channels == 2
199+
decoded = decoder.get_all_samples()
200+
assert decoded.data.shape == (2, NUM_SAMPLES)
201+
torch.testing.assert_close(decoded.data, samples, atol=1e-4, rtol=1e-3)
202+
203+
def test_to_tensor(self):
204+
samples = torch.rand(1, NUM_SAMPLES) * 2 - 1
205+
encoded = AudioEncoder(samples, sample_rate=SAMPLE_RATE).to_tensor(format="wav")
206+
assert encoded.dtype == torch.uint8
207+
assert encoded.ndim == 1
208+
assert len(encoded) > 0
209+
210+
decoder = AudioDecoder(encoded)
211+
decoded = decoder.get_all_samples()
212+
assert decoded.data.shape == (1, NUM_SAMPLES)
213+
torch.testing.assert_close(decoded.data, samples, atol=1e-4, rtol=1e-3)
214+
215+
def test_mono_1d_input(self, tmp_path):
216+
samples = torch.rand(NUM_SAMPLES) * 2 - 1
217+
path = str(tmp_path / "mono.wav")
218+
AudioEncoder(samples, sample_rate=SAMPLE_RATE).to_file(path)
219+
220+
decoder = AudioDecoder(path)
221+
assert decoder.metadata.num_channels == 1
222+
decoded = decoder.get_all_samples()
223+
assert decoded.data.shape == (1, NUM_SAMPLES)
224+
torch.testing.assert_close(decoded.data[0], samples, atol=1e-4, rtol=1e-3)
225+
226+
def test_resample_on_encode(self, tmp_path):
227+
samples = torch.rand(1, NUM_SAMPLES) * 2 - 1
228+
path = str(tmp_path / "resampled.wav")
229+
AudioEncoder(samples, sample_rate=SAMPLE_RATE).to_file(path, sample_rate=8000)
230+
decoder = AudioDecoder(path)
231+
assert decoder.metadata.sample_rate == 8000
232+
decoded = decoder.get_all_samples()
233+
assert decoded.data.shape[0] == 1
234+
expected_num_samples = int(NUM_SAMPLES * 8000 / SAMPLE_RATE)
235+
assert abs(decoded.data.shape[1] - expected_num_samples) <= 1
236+
237+
238+
class TestStreamingEncoder:
239+
def test_video_and_audio_chunked(self, tmp_path):
240+
frames = torch.randint(
241+
0, 256, (NUM_FRAMES, 3, HEIGHT, WIDTH), dtype=torch.uint8
242+
)
243+
samples = torch.rand(NUM_AUDIO_CHANNELS, NUM_SAMPLES) * 2 - 1
244+
path = tmp_path / "av.mkv"
245+
246+
enc = StreamingEncoder()
247+
video = enc.add_video(
248+
height=HEIGHT,
249+
width=WIDTH,
250+
frame_rate=FRAME_RATE,
251+
pixel_format="yuv444p",
252+
crf=0,
253+
)
254+
audio = enc.add_audio(sample_rate=SAMPLE_RATE, num_channels=NUM_AUDIO_CHANNELS)
255+
enc.open(dest=path)
256+
with enc:
257+
video.write(frames[:5])
258+
audio.write(samples[:, : NUM_SAMPLES // 2])
259+
video.write(frames[5:])
260+
audio.write(samples[:, NUM_SAMPLES // 2 :])
261+
262+
video_dec = VideoDecoder(path)
263+
assert len(video_dec) == NUM_FRAMES
264+
decoded_frames = video_dec.get_all_frames()
265+
torch.testing.assert_close(decoded_frames.data, frames, atol=2, rtol=0)
266+
267+
audio_dec = AudioDecoder(path)
268+
assert audio_dec.metadata.num_channels == NUM_AUDIO_CHANNELS
269+
assert audio_dec.metadata.sample_rate == SAMPLE_RATE
270+
decoded_samples = audio_dec.get_all_samples()
271+
assert decoded_samples.data.shape[0] == NUM_AUDIO_CHANNELS
272+
assert decoded_samples.sample_rate == SAMPLE_RATE
273+
# TODO: validate audio on a mostly lossless codec?
274+
275+
@pytest.mark.needs_cuda
276+
def test_cuda_encoding(self, tmp_path):
277+
frames = torch.randint(
278+
0, 256, (NUM_FRAMES, 3, HEIGHT, WIDTH), dtype=torch.uint8, device="cuda"
279+
)
280+
path = tmp_path / "cuda.mp4"
281+
282+
enc = StreamingEncoder()
283+
video = enc.add_video(
284+
height=HEIGHT, width=WIDTH, frame_rate=FRAME_RATE, device="cuda"
285+
)
286+
enc.open(dest=path)
287+
with enc:
288+
video.write(frames)
289+
290+
decoder = VideoDecoder(path)
291+
assert len(decoder) == NUM_FRAMES
292+
decoded = decoder.get_all_frames()
293+
assert decoded.data.shape == (NUM_FRAMES, 3, HEIGHT, WIDTH)
294+
torch.testing.assert_close(decoded.data, frames.cpu(), atol=5, rtol=0)

0 commit comments

Comments
 (0)