From c029c493ba69c48acef1d9d9367581ef030167f1 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Mon, 2 Feb 2026 14:51:22 -0500 Subject: [PATCH 1/7] changes, demonstrative test --- src/torchcodec/_core/Encoder.cpp | 34 +++++++++++++++- test/test_encoders.py | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/torchcodec/_core/Encoder.cpp b/src/torchcodec/_core/Encoder.cpp index 8093f6b4b..e9866ca62 100644 --- a/src/torchcodec/_core/Encoder.cpp +++ b/src/torchcodec/_core/Encoder.cpp @@ -631,13 +631,17 @@ void tryToValidateCodecOption( } void sortCodecOptions( + const AVFormatContext* avFormatContext, const std::map& extraOptions, UniqueAVDictionary& codecDict, UniqueAVDictionary& formatDict) { // Accepts a map of options as input, then sorts them into codec options and // format options. The sorted options are returned into two separate dicts. const AVClass* formatClass = avformat_get_class(); + const AVClass* muxerClass = + avFormatContext->oformat ? avFormatContext->oformat->priv_class : nullptr; for (const auto& [key, value] : extraOptions) { + // Check if option is generic format option const AVOption* fmtOpt = av_opt_find2( &formatClass, key.c_str(), @@ -645,10 +649,24 @@ void sortCodecOptions( 0, AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ, nullptr); - if (fmtOpt) { + // Check if option is muxer-specific option + // (Returned from `ffmpeg -h muxer=mp4`) + const AVOption* muxerOpt = nullptr; + if (muxerClass) { + muxerOpt = av_opt_find2( + &muxerClass, + key.c_str(), + nullptr, + 0, + AV_OPT_SEARCH_FAKE_OBJ, + nullptr); + } + if (fmtOpt || muxerOpt) { + // Pass container-format options to formatDict to be used in + // avformat_write_header av_dict_set(formatDict.getAddress(), key.c_str(), value.c_str(), 0); } else { - // Default to codec option (includes AVCodecContext + encoder-private) + // By default, pass as codec option to be used in avcodec_open2 av_dict_set(codecDict.getAddress(), key.c_str(), value.c_str(), 0); } } @@ -835,6 +853,7 @@ void VideoEncoder::initializeEncoder( tryToValidateCodecOption(*avCodec, key.c_str(), value); } sortCodecOptions( + avFormatContext_.get(), videoStreamOptions.extraOptions.value(), avCodecOptions, avFormatOptions_); @@ -921,6 +940,17 @@ void VideoEncoder::encode() { flushBuffers(); status = av_write_trailer(avFormatContext_.get()); + // Fragmented video containers write an mfra atom at the end of the file. + // mfra stands for "Movie Fragment Random Access". + // When writing a fragmented video file, the return value of + // mov_write_mfra_tag is returned. It is negative when an error occurs. When + // its positive, it represents the byte size of the written mfra atom. We will + // erronously interpret this as an error, so we replace positive values with + // AVSUCCESS. See: + // https://github.com/FFmpeg/FFmpeg/blob/n8.0/libavformat/movenc.c#L8666 + if (status > 0) { + status = AVSUCCESS; + } TORCH_CHECK( status == AVSUCCESS, "Error in av_write_trailer: ", diff --git a/test/test_encoders.py b/test/test_encoders.py index 7b3279aa5..e38393a8a 100644 --- a/test/test_encoders.py +++ b/test/test_encoders.py @@ -1511,3 +1511,69 @@ def test_nvenc_against_ffmpeg_cli( assert color_range == encoder_metadata["color_range"] if color_space is not None: assert color_space == encoder_metadata["color_space"] + + @pytest.mark.parametrize("format", ["mp4", "mov"]) + @pytest.mark.parametrize("truncate_percent", [0.5]) + @pytest.mark.parametrize( + "extra_options", + [ + # frag_keyframe with empty_moov (standard fragmented MP4) + {"movflags": "+frag_keyframe+empty_moov"}, + # frag_duration creates fragments based on duration (in microseconds) + {"movflags": "+empty_moov", "frag_duration": "1000000"}, + ], + ) + def test_fragmented_mp4( + self, + # tmp_path, + extra_options, + format, + truncate_percent, + ): + # Test that VideoEncoder can write fragmented files using movflags. + # Fragmented files store metadata interleaved with data rather than + # all at the end, making them decodable even if writing is interrupted. + # The mov muxer (used for mp4, mov) supports these options. + tmp_path = Path("tmp") + source_frames, frame_rate = self.decode_and_get_frame_rate(TEST_SRC_2_720P.path) + encoder = VideoEncoder(frames=source_frames, frame_rate=frame_rate) + encoded_path = str(tmp_path / f"fragmented_output.{format}") + encoder.to_file(dest=encoded_path, extra_options=extra_options) + + # Truncate the file to simulate interrupted write + with open(encoded_path, "rb") as f: + full_content = f.read() + truncated_size = int(len(full_content) * (1 - truncate_percent)) + with open(encoded_path, "wb") as f: + f.write(full_content[:truncated_size]) + + # Verify truncated file is still readable by ffprobe + result = subprocess.run( + ["ffprobe", "-v", "error", "-show_streams", "-of", "json", encoded_path], + capture_output=True, + text=True, + ) + probe_data = json.loads(result.stdout) + + # Verify video stream dimensions match original frames + streams = probe_data.get("streams", []) + video_streams = [s for s in streams if s.get("codec_type") == "video"] + assert len(video_streams) == 1 + video_stream = video_streams[0] + assert video_stream.get("width") == source_frames.shape[3] + assert video_stream.get("height") == source_frames.shape[2] + + # Decode as many frames as possible with VideoEncoder in approximate mode + decoder = VideoDecoder(encoded_path, seek_mode="approximate") + for i in range(len(decoder)): + try: + decoder.get_frame_at(i) + print(f"Decoded frame {i}") + except RuntimeError as e: + print(f"Failed to decode frame {i}: {e}") + break + + # VideoDecoder will fail to initialize in exact mode on truncated file + with pytest.raises(RuntimeError) as error: + VideoDecoder(encoded_path, seek_mode="exact") + print(f"Failure occurred in exact mode: {error.value}") From 3cfacf5b060c576ba58800b3370beafd1e5964f7 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Mon, 2 Feb 2026 16:39:01 -0500 Subject: [PATCH 2/7] update test --- test/test_encoders.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/test/test_encoders.py b/test/test_encoders.py index e38393a8a..c02ac62a8 100644 --- a/test/test_encoders.py +++ b/test/test_encoders.py @@ -1525,7 +1525,7 @@ def test_nvenc_against_ffmpeg_cli( ) def test_fragmented_mp4( self, - # tmp_path, + tmp_path, extra_options, format, truncate_percent, @@ -1534,7 +1534,6 @@ def test_fragmented_mp4( # Fragmented files store metadata interleaved with data rather than # all at the end, making them decodable even if writing is interrupted. # The mov muxer (used for mp4, mov) supports these options. - tmp_path = Path("tmp") source_frames, frame_rate = self.decode_and_get_frame_rate(TEST_SRC_2_720P.path) encoder = VideoEncoder(frames=source_frames, frame_rate=frame_rate) encoded_path = str(tmp_path / f"fragmented_output.{format}") @@ -1563,17 +1562,20 @@ def test_fragmented_mp4( assert video_stream.get("width") == source_frames.shape[3] assert video_stream.get("height") == source_frames.shape[2] - # Decode as many frames as possible with VideoEncoder in approximate mode - decoder = VideoDecoder(encoded_path, seek_mode="approximate") - for i in range(len(decoder)): + # Decode as many frames as possible with VideoDecoder in each seek mode + for seek_mode in ["approximate", "exact"]: try: - decoder.get_frame_at(i) - print(f"Decoded frame {i}") + decoder = VideoDecoder(encoded_path, seek_mode=seek_mode) except RuntimeError as e: - print(f"Failed to decode frame {i}: {e}") - break + print(f"seek_mode={seek_mode}: Failed to initialize decoder: {e}") + continue - # VideoDecoder will fail to initialize in exact mode on truncated file - with pytest.raises(RuntimeError) as error: - VideoDecoder(encoded_path, seek_mode="exact") - print(f"Failure occurred in exact mode: {error.value}") + for i in range(len(decoder)): + try: + decoder.get_frame_at(i) + except RuntimeError: + break + else: + # Hit if all frames decoded successfully + i = len(decoder) + print(f"seek_mode={seek_mode}: decoded {i}/{len(decoder)} frames") From c35d1e8491882c2960ddca8653af72f93e78297a Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Mon, 2 Feb 2026 16:39:54 -0500 Subject: [PATCH 3/7] concurrent TEST script --- scripts/test_concurrent_encode_decode.py | 173 +++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 scripts/test_concurrent_encode_decode.py diff --git a/scripts/test_concurrent_encode_decode.py b/scripts/test_concurrent_encode_decode.py new file mode 100644 index 000000000..5171bfdba --- /dev/null +++ b/scripts/test_concurrent_encode_decode.py @@ -0,0 +1,173 @@ +""" +Test concurrent encoding and decoding of fragmented MP4. + +This script encodes a video with fragmented MP4 options while repeatedly +attempting to decode the file from another process to test read-while-write. +""" + +import multiprocessing +import os +import time + +import torch + + +def get_test_frames(): + """Generate test frames - colored gradient frames.""" + num_frames = 900 # More frames = longer encoding time + height, width = 1920, 1080 # Full HD for slower encoding + frames = torch.zeros((num_frames, 3, height, width), dtype=torch.uint8) + + for i in range(num_frames): + # Create a gradient that changes per frame + r = int(255 * i / num_frames) + g = int(255 * (1 - i / num_frames)) + b = 128 + frames[i, 0, :, :] = r # R channel + frames[i, 1, :, :] = g # G channel + frames[i, 2, :, :] = b # B channel + + return frames + + +def writer_process(path: str, ready_event, done_event): + """Encode frames to a fragmented MP4 file.""" + from torchcodec.encoders import VideoEncoder + + print(f"[WRITER] Starting encoder, output: {path}") + frames = get_test_frames() + print(f"[WRITER] Generated {len(frames)} frames of shape {frames.shape}") + + encoder = VideoEncoder(frames=frames, frame_rate=30.0) + + # Signal that we're about to start encoding + ready_event.set() + + start_time = time.time() + encoder.to_file( + dest=path, + preset="slow", # Slower preset = more time to test concurrent reads + extra_options={ + "movflags": "+frag_keyframe+empty_moov", + "frag_duration": "100000", # Fragment every 100ms + }, + ) + elapsed = time.time() - start_time + + print(f"[WRITER] Encoding complete in {elapsed:.2f}s") + done_event.set() + + +def reader_process(path: str, ready_event, done_event): + """Repeatedly attempt to decode the file while it's being written.""" + from torchcodec.decoders import VideoDecoder + + # Wait for writer to be ready + ready_event.wait() + print("[READER] Writer is ready, starting decode attempts") + + attempt = 0 + last_file_size = 0 + last_frame_count = 0 + + while not done_event.is_set() or attempt < 3: # A few extra attempts after done + attempt += 1 + time.sleep(0.1) # Check every 100ms + + # Check file existence and size + if not os.path.exists(path): + print(f"[READER] Attempt {attempt}: File does not exist yet") + continue + + file_size = os.path.getsize(path) + size_delta = file_size - last_file_size + last_file_size = file_size + + try: + # Try to open with approximate seek mode (more tolerant of incomplete files) + decoder = VideoDecoder(path, seek_mode="approximate") + num_frames = len(decoder) + + # Try to decode frames + decoded_count = 0 + for i in range(num_frames): + try: + decoder.get_frame_at(i) + decoded_count += 1 + except RuntimeError: + # Stop at first decode error + break + + frame_delta = decoded_count - last_frame_count + last_frame_count = decoded_count + + print( + f"[READER] Attempt {attempt}: " + f"file_size={file_size:,} bytes (+{size_delta:,}), " + f"reported_frames={num_frames}, " + f"decoded_frames={decoded_count} (+{frame_delta})" + ) + + except RuntimeError as e: + error_msg = str(e)[:80] # Truncate long error messages + print( + f"[READER] Attempt {attempt}: " + f"file_size={file_size:,} bytes (+{size_delta:,}), " + f"error={error_msg}" + ) + except Exception as e: + print( + f"[READER] Attempt {attempt}: Unexpected error: {type(e).__name__}: {e}" + ) + + print( + f"[READER] Done after {attempt} attempts, final decoded frames: {last_frame_count}" + ) + + +def main(): + output_path = "/tmp/concurrent_test.mp4" + + # Clean up from previous runs + if os.path.exists(output_path): + os.remove(output_path) + + # Create synchronization events + ready_event = multiprocessing.Event() + done_event = multiprocessing.Event() + + # Start processes + writer = multiprocessing.Process( + target=writer_process, args=(output_path, ready_event, done_event) + ) + reader = multiprocessing.Process( + target=reader_process, args=(output_path, ready_event, done_event) + ) + + print("Starting concurrent encode/decode test...") + print(f"Output file: {output_path}") + print("-" * 60) + + writer.start() + reader.start() + + writer.join() + reader.join() + + print("-" * 60) + + # Final verification + if os.path.exists(output_path): + final_size = os.path.getsize(output_path) + print(f"Final file size: {final_size:,} bytes") + + from torchcodec.decoders import VideoDecoder + + decoder = VideoDecoder(output_path) + print(f"Final frame count: {len(decoder)}") + else: + print("ERROR: Output file was not created") + + +if __name__ == "__main__": + main() From 8ed1de72e11e1fec41e69d9564c3579f9235a0ef Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 3 Feb 2026 15:26:59 -0500 Subject: [PATCH 4/7] test frame exactness in test --- src/torchcodec/_core/Encoder.cpp | 10 ++----- test/test_encoders.py | 50 ++++++++------------------------ 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/src/torchcodec/_core/Encoder.cpp b/src/torchcodec/_core/Encoder.cpp index e9866ca62..e53f399e9 100644 --- a/src/torchcodec/_core/Encoder.cpp +++ b/src/torchcodec/_core/Encoder.cpp @@ -940,13 +940,9 @@ void VideoEncoder::encode() { flushBuffers(); status = av_write_trailer(avFormatContext_.get()); - // Fragmented video containers write an mfra atom at the end of the file. - // mfra stands for "Movie Fragment Random Access". - // When writing a fragmented video file, the return value of - // mov_write_mfra_tag is returned. It is negative when an error occurs. When - // its positive, it represents the byte size of the written mfra atom. We will - // erronously interpret this as an error, so we replace positive values with - // AVSUCCESS. See: + // av_write_trailer returns mfra atom size (positive) for fragmented + // containers, which we'd misinterpret as an error. So we replace positive + // values with AVSUCCESS. See: // https://github.com/FFmpeg/FFmpeg/blob/n8.0/libavformat/movenc.c#L8666 if (status > 0) { status = AVSUCCESS; diff --git a/test/test_encoders.py b/test/test_encoders.py index 19d4b8c25..2bb4cad74 100644 --- a/test/test_encoders.py +++ b/test/test_encoders.py @@ -1511,11 +1511,10 @@ def test_nvenc_against_ffmpeg_cli( assert color_space == encoder_metadata["color_space"] @pytest.mark.parametrize("format", ["mp4", "mov"]) - @pytest.mark.parametrize("truncate_percent", [0.5]) @pytest.mark.parametrize( "extra_options", [ - # frag_keyframe with empty_moov (standard fragmented MP4) + # frag_keyframe with empty_moov (new fragment every keyframe) {"movflags": "+frag_keyframe+empty_moov"}, # frag_duration creates fragments based on duration (in microseconds) {"movflags": "+empty_moov", "frag_duration": "1000000"}, @@ -1526,54 +1525,29 @@ def test_fragmented_mp4( tmp_path, extra_options, format, - truncate_percent, ): # Test that VideoEncoder can write fragmented files using movflags. # Fragmented files store metadata interleaved with data rather than # all at the end, making them decodable even if writing is interrupted. - # The mov muxer (used for mp4, mov) supports these options. source_frames, frame_rate = self.decode_and_get_frame_rate(TEST_SRC_2_720P.path) encoder = VideoEncoder(frames=source_frames, frame_rate=frame_rate) encoded_path = str(tmp_path / f"fragmented_output.{format}") encoder.to_file(dest=encoded_path, extra_options=extra_options) + # Decode the file to get reference frames + reference_decoder = VideoDecoder(encoded_path) + reference_frames = [reference_decoder.get_frame_at(i) for i in range(10)] + # Truncate the file to simulate interrupted write with open(encoded_path, "rb") as f: full_content = f.read() - truncated_size = int(len(full_content) * (1 - truncate_percent)) + truncated_size = int(len(full_content) * 0.5) with open(encoded_path, "wb") as f: f.write(full_content[:truncated_size]) - # Verify truncated file is still readable by ffprobe - result = subprocess.run( - ["ffprobe", "-v", "error", "-show_streams", "-of", "json", encoded_path], - capture_output=True, - text=True, - ) - probe_data = json.loads(result.stdout) - - # Verify video stream dimensions match original frames - streams = probe_data.get("streams", []) - video_streams = [s for s in streams if s.get("codec_type") == "video"] - assert len(video_streams) == 1 - video_stream = video_streams[0] - assert video_stream.get("width") == source_frames.shape[3] - assert video_stream.get("height") == source_frames.shape[2] - - # Decode as many frames as possible with VideoDecoder in each seek mode - for seek_mode in ["approximate", "exact"]: - try: - decoder = VideoDecoder(encoded_path, seek_mode=seek_mode) - except RuntimeError as e: - print(f"seek_mode={seek_mode}: Failed to initialize decoder: {e}") - continue - - for i in range(len(decoder)): - try: - decoder.get_frame_at(i) - except RuntimeError: - break - else: - # Hit if all frames decoded successfully - i = len(decoder) - print(f"seek_mode={seek_mode}: decoded {i}/{len(decoder)} frames") + # Decode the truncated file and verify first 10 frames match reference + truncated_decoder = VideoDecoder(encoded_path) + assert len(truncated_decoder) >= 10 + for i in range(10): + truncated_frame = truncated_decoder.get_frame_at(i) + assert torch.equal(truncated_frame.data, reference_frames[i].data) From 624d4ff229487db9ab66ef7d416bb9f319a539df Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 3 Feb 2026 15:30:21 -0500 Subject: [PATCH 5/7] Revert "concurrent TEST script" This reverts commit c35d1e8491882c2960ddca8653af72f93e78297a. --- scripts/test_concurrent_encode_decode.py | 173 ----------------------- 1 file changed, 173 deletions(-) delete mode 100644 scripts/test_concurrent_encode_decode.py diff --git a/scripts/test_concurrent_encode_decode.py b/scripts/test_concurrent_encode_decode.py deleted file mode 100644 index 5171bfdba..000000000 --- a/scripts/test_concurrent_encode_decode.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -Test concurrent encoding and decoding of fragmented MP4. - -This script encodes a video with fragmented MP4 options while repeatedly -attempting to decode the file from another process to test read-while-write. -""" - -import multiprocessing -import os -import time - -import torch - - -def get_test_frames(): - """Generate test frames - colored gradient frames.""" - num_frames = 900 # More frames = longer encoding time - height, width = 1920, 1080 # Full HD for slower encoding - frames = torch.zeros((num_frames, 3, height, width), dtype=torch.uint8) - - for i in range(num_frames): - # Create a gradient that changes per frame - r = int(255 * i / num_frames) - g = int(255 * (1 - i / num_frames)) - b = 128 - frames[i, 0, :, :] = r # R channel - frames[i, 1, :, :] = g # G channel - frames[i, 2, :, :] = b # B channel - - return frames - - -def writer_process(path: str, ready_event, done_event): - """Encode frames to a fragmented MP4 file.""" - from torchcodec.encoders import VideoEncoder - - print(f"[WRITER] Starting encoder, output: {path}") - frames = get_test_frames() - print(f"[WRITER] Generated {len(frames)} frames of shape {frames.shape}") - - encoder = VideoEncoder(frames=frames, frame_rate=30.0) - - # Signal that we're about to start encoding - ready_event.set() - - start_time = time.time() - encoder.to_file( - dest=path, - preset="slow", # Slower preset = more time to test concurrent reads - extra_options={ - "movflags": "+frag_keyframe+empty_moov", - "frag_duration": "100000", # Fragment every 100ms - }, - ) - elapsed = time.time() - start_time - - print(f"[WRITER] Encoding complete in {elapsed:.2f}s") - done_event.set() - - -def reader_process(path: str, ready_event, done_event): - """Repeatedly attempt to decode the file while it's being written.""" - from torchcodec.decoders import VideoDecoder - - # Wait for writer to be ready - ready_event.wait() - print("[READER] Writer is ready, starting decode attempts") - - attempt = 0 - last_file_size = 0 - last_frame_count = 0 - - while not done_event.is_set() or attempt < 3: # A few extra attempts after done - attempt += 1 - time.sleep(0.1) # Check every 100ms - - # Check file existence and size - if not os.path.exists(path): - print(f"[READER] Attempt {attempt}: File does not exist yet") - continue - - file_size = os.path.getsize(path) - size_delta = file_size - last_file_size - last_file_size = file_size - - try: - # Try to open with approximate seek mode (more tolerant of incomplete files) - decoder = VideoDecoder(path, seek_mode="approximate") - num_frames = len(decoder) - - # Try to decode frames - decoded_count = 0 - for i in range(num_frames): - try: - decoder.get_frame_at(i) - decoded_count += 1 - except RuntimeError: - # Stop at first decode error - break - - frame_delta = decoded_count - last_frame_count - last_frame_count = decoded_count - - print( - f"[READER] Attempt {attempt}: " - f"file_size={file_size:,} bytes (+{size_delta:,}), " - f"reported_frames={num_frames}, " - f"decoded_frames={decoded_count} (+{frame_delta})" - ) - - except RuntimeError as e: - error_msg = str(e)[:80] # Truncate long error messages - print( - f"[READER] Attempt {attempt}: " - f"file_size={file_size:,} bytes (+{size_delta:,}), " - f"error={error_msg}" - ) - except Exception as e: - print( - f"[READER] Attempt {attempt}: Unexpected error: {type(e).__name__}: {e}" - ) - - print( - f"[READER] Done after {attempt} attempts, final decoded frames: {last_frame_count}" - ) - - -def main(): - output_path = "/tmp/concurrent_test.mp4" - - # Clean up from previous runs - if os.path.exists(output_path): - os.remove(output_path) - - # Create synchronization events - ready_event = multiprocessing.Event() - done_event = multiprocessing.Event() - - # Start processes - writer = multiprocessing.Process( - target=writer_process, args=(output_path, ready_event, done_event) - ) - reader = multiprocessing.Process( - target=reader_process, args=(output_path, ready_event, done_event) - ) - - print("Starting concurrent encode/decode test...") - print(f"Output file: {output_path}") - print("-" * 60) - - writer.start() - reader.start() - - writer.join() - reader.join() - - print("-" * 60) - - # Final verification - if os.path.exists(output_path): - final_size = os.path.getsize(output_path) - print(f"Final file size: {final_size:,} bytes") - - from torchcodec.decoders import VideoDecoder - - decoder = VideoDecoder(output_path) - print(f"Final frame count: {len(decoder)}") - else: - print("ERROR: Output file was not created") - - -if __name__ == "__main__": - main() From 4b4bf72093fae6fdcef6268099c275967904b6c8 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Tue, 3 Feb 2026 17:05:44 -0500 Subject: [PATCH 6/7] skip tes on ffmpeg4 --- test/test_encoders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_encoders.py b/test/test_encoders.py index 2bb4cad74..af1aee8d2 100644 --- a/test/test_encoders.py +++ b/test/test_encoders.py @@ -1510,6 +1510,10 @@ def test_nvenc_against_ffmpeg_cli( if color_space is not None: assert color_space == encoder_metadata["color_space"] + @pytest.mark.skipif( + ffmpeg_major_version == 4, + reason="On FFmpeg 4 we error on truncated packets", + ) @pytest.mark.parametrize("format", ["mp4", "mov"]) @pytest.mark.parametrize( "extra_options", From 4941c73780671cbcf86eb075d35efed0fb492c36 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Thu, 12 Feb 2026 14:36:52 -0500 Subject: [PATCH 7/7] suggetstions --- src/torchcodec/_core/Encoder.cpp | 4 +++- test/test_encoders.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/torchcodec/_core/Encoder.cpp b/src/torchcodec/_core/Encoder.cpp index e53f399e9..539c60a98 100644 --- a/src/torchcodec/_core/Encoder.cpp +++ b/src/torchcodec/_core/Encoder.cpp @@ -941,7 +941,9 @@ void VideoEncoder::encode() { status = av_write_trailer(avFormatContext_.get()); // av_write_trailer returns mfra atom size (positive) for fragmented - // containers, which we'd misinterpret as an error. So we replace positive + // containers, which we'd misinterpret as an error, since all FFmpeg errors + // are negative (see AVERROR definition: + // http://ffmpeg.org/doxygen/8.0/error_8h_source.html) So we replace positive // values with AVSUCCESS. See: // https://github.com/FFmpeg/FFmpeg/blob/n8.0/libavformat/movenc.c#L8666 if (status > 0) { diff --git a/test/test_encoders.py b/test/test_encoders.py index af1aee8d2..fbb080998 100644 --- a/test/test_encoders.py +++ b/test/test_encoders.py @@ -1512,7 +1512,7 @@ def test_nvenc_against_ffmpeg_cli( @pytest.mark.skipif( ffmpeg_major_version == 4, - reason="On FFmpeg 4 we error on truncated packets", + reason="On FFmpeg 4 hitting a truncated packet results in AVERROR_INVALIDDATA, which torchcodec does not handle.", ) @pytest.mark.parametrize("format", ["mp4", "mov"]) @pytest.mark.parametrize( @@ -1554,4 +1554,6 @@ def test_fragmented_mp4( assert len(truncated_decoder) >= 10 for i in range(10): truncated_frame = truncated_decoder.get_frame_at(i) - assert torch.equal(truncated_frame.data, reference_frames[i].data) + torch.testing.assert_close( + truncated_frame.data, reference_frames[i].data, atol=0, rtol=0 + )