From f802b38e9c830c69728a455f6f7ad93e5bbbea09 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Mon, 28 Jul 2025 10:44:04 -0400 Subject: [PATCH 01/22] Update VideoDecoder init --- src/torchcodec/decoders/_video_decoder.py | 45 +++++++++++++ test/test_decoders.py | 81 +++++++++++++++++++++++ test/utils.py | 40 +++++------ 3 files changed, 147 insertions(+), 19 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 0a030dbd0..b2da7c9ec 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -5,6 +5,7 @@ # LICENSE file in the root directory of this source tree. import io +import json import numbers from pathlib import Path from typing import Literal, Optional, Tuple, Union @@ -80,6 +81,7 @@ def __init__( num_ffmpeg_threads: int = 1, device: Optional[Union[str, torch_device]] = "cpu", seek_mode: Literal["exact", "approximate"] = "exact", + custom_frame_mappings: Optional[Union[Path, str]] = None, ): torch._C._log_api_usage_once("torchcodec.decoders.VideoDecoder") allowed_seek_modes = ("exact", "approximate") @@ -88,6 +90,16 @@ def __init__( f"Invalid seek mode ({seek_mode}). " f"Supported values are {', '.join(allowed_seek_modes)}." ) + if custom_frame_mappings and seek_mode != "exact": + raise ValueError( + "Custom frame mappings are only supported in 'exact' seek mode." + "While setting custom frame mappings, do not set `seek_mode`." + ) + custom_frame_mappings_data = ( + read_custom_frame_mappings(custom_frame_mappings) + if custom_frame_mappings is not None + else None + ) self._decoder = create_decoder(source=source, seek_mode=seek_mode) @@ -110,6 +122,7 @@ def __init__( dimension_order=dimension_order, num_threads=num_ffmpeg_threads, device=device, + custom_frame_mappings=custom_frame_mappings_data, ) ( @@ -379,3 +392,35 @@ def _get_and_validate_stream_metadata( end_stream_seconds, num_frames, ) + + +def read_custom_frame_mappings( + custom_frame_mappings: Union[Path, str] +) -> tuple[Tensor, Tensor, Tensor]: + try: + if hasattr(custom_frame_mappings, "read"): + input_data = json.load(custom_frame_mappings) + else: + input_data = json.loads(custom_frame_mappings) + except json.JSONDecodeError: + raise ValueError( + "Invalid custom frame mappings. " + "It should be a valid JSON string or a path to a JSON file." + ) + all_frames, is_key_frame, duration = zip( + *[ + (float(frame["pts"]), frame["key_frame"], float(frame["duration"])) + for frame in input_data["frames"] + ] + ) + all_frames = Tensor(all_frames) + is_key_frame = Tensor(is_key_frame) + duration = Tensor(duration) + assert ( + len(all_frames) == len(is_key_frame) == len(duration) + ), "Mismatched lengths in frame index data" + return ( + all_frames, + is_key_frame, + duration, + ) diff --git a/test/test_decoders.py b/test/test_decoders.py index d3f5ebc00..bf19bf1d8 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -7,6 +7,7 @@ import contextlib import gc import json +from functools import partial from unittest.mock import patch import numpy @@ -1279,6 +1280,86 @@ def test_10bit_videos_cpu(self, asset): decoder = VideoDecoder(asset.path) decoder.get_frame_at(10) + def setup_frame_mappings(tmp_path: str, file: bool, stream_index: int): + json_path = tmp_path / "custom_frame_mappings.json" + custom_frame_mappings = NASA_VIDEO.generate_custom_frame_mappings(stream_index) + if file: + # Write the custom frame mappings to a JSON file + with open(json_path, "w") as f: + f.write(custom_frame_mappings) + return json_path + else: + # Return the custom frame mappings as a JSON string + return custom_frame_mappings + + @pytest.mark.parametrize("device", cpu_and_cuda()) + @pytest.mark.parametrize("stream_index", [0, 3]) + @pytest.mark.parametrize( + "method", + ( + partial(setup_frame_mappings, file=True), + partial(setup_frame_mappings, file=False), + ), + ) + def test_custom_frame_mappings(self, tmp_path, device, stream_index, method): + custom_frame_mappings = method(tmp_path=tmp_path, stream_index=stream_index) + # Optionally open the custom frame mappings file if it is a file path + # or use a null context if it is a string. + with ( + open(custom_frame_mappings, "r") + if hasattr(custom_frame_mappings, "read") + else contextlib.nullcontext() + ) as custom_frame_mappings: + decoder = VideoDecoder( + NASA_VIDEO.path, + stream_index=stream_index, + device=device, + custom_frame_mappings=custom_frame_mappings, + ) + frame_0 = decoder.get_frame_at(0) + frame_5 = decoder.get_frame_at(5) + assert_frames_equal( + frame_0.data, + NASA_VIDEO.get_frame_data_by_index(0, stream_index=stream_index).to( + device + ), + ) + assert_frames_equal( + frame_5.data, + NASA_VIDEO.get_frame_data_by_index(5, stream_index=stream_index).to( + device + ), + ) + frames0_5 = decoder.get_frames_played_in_range( + frame_0.pts_seconds, frame_5.pts_seconds + ) + assert_frames_equal( + frames0_5.data, + NASA_VIDEO.get_frame_data_by_range(0, 5, stream_index=stream_index).to( + device + ), + ) + + decoder = VideoDecoder( + H265_VIDEO.path, + stream_index=0, + custom_frame_mappings=H265_VIDEO.generate_custom_frame_mappings(0), + ) + ref_frame6 = H265_VIDEO.get_frame_data_by_index(5) + assert_frames_equal(ref_frame6, decoder.get_frame_played_at(0.5).data) + + @pytest.mark.parametrize("device", cpu_and_cuda()) + def test_custom_frame_mappings_init_fails(self, device): + # Init fails if "approximate" seek mode is used with custom frame mappings + with pytest.raises(ValueError, match="seek_mode"): + VideoDecoder( + NASA_VIDEO.path, + stream_index=3, + device=device, + seek_mode="approximate", + custom_frame_mappings=NASA_VIDEO.generate_custom_frame_mappings(3), + ) + class TestAudioDecoder: @pytest.mark.parametrize("asset", (NASA_AUDIO, NASA_AUDIO_MP3, SINE_MONO_S32)) diff --git a/test/utils.py b/test/utils.py index ed611cfda..ab5d0b77e 100644 --- a/test/utils.py +++ b/test/utils.py @@ -267,27 +267,29 @@ def get_custom_frame_mappings( if stream_index is None: stream_index = self.default_stream_index if self._custom_frame_mappings_data.get(stream_index) is None: - self.generate_custom_frame_mappings(stream_index) + self.create_custom_frame_mappings(stream_index) return self._custom_frame_mappings_data[stream_index] - def generate_custom_frame_mappings(self, stream_index: int) -> None: - result = json.loads( - subprocess.run( - [ - "ffprobe", - "-i", - f"{self.path}", - "-select_streams", - f"{stream_index}", - "-show_frames", - "-of", - "json", - ], - check=True, - capture_output=True, - text=True, - ).stdout - ) + def generate_custom_frame_mappings(self, stream_index: int) -> str: + result = subprocess.run( + [ + "ffprobe", + "-i", + f"{self.path}", + "-select_streams", + f"{stream_index}", + "-show_frames", + "-of", + "json", + ], + check=True, + capture_output=True, + text=True, + ).stdout + return result + + def create_custom_frame_mappings(self, stream_index: int) -> None: + result = json.loads(self.generate_custom_frame_mappings(stream_index)) all_frames = torch.tensor([float(frame["pts"]) for frame in result["frames"]]) is_key_frame = torch.tensor([frame["key_frame"] for frame in result["frames"]]) duration = torch.tensor( From e4226ad557add7aa534009081d24819433902cad Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 30 Jul 2025 10:30:06 -0400 Subject: [PATCH 02/22] Update seek_mode passed to C++ when frame_mappings used --- src/torchcodec/_core/SingleStreamDecoder.cpp | 2 +- src/torchcodec/decoders/_video_decoder.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/torchcodec/_core/SingleStreamDecoder.cpp b/src/torchcodec/_core/SingleStreamDecoder.cpp index 9cfc652ad..c61872b7c 100644 --- a/src/torchcodec/_core/SingleStreamDecoder.cpp +++ b/src/torchcodec/_core/SingleStreamDecoder.cpp @@ -506,7 +506,7 @@ void SingleStreamDecoder::addVideoStream( if (seekMode_ == SeekMode::custom_frame_mappings) { TORCH_CHECK( customFrameMappings.has_value(), - "Please provide frame mappings when using custom_frame_mappings seek mode."); + "Missing frame mappings when custom_frame_mappings seek mode is set."); readCustomFrameMappingsUpdateMetadataAndIndex( streamIndex, customFrameMappings.value()); } diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index b2da7c9ec..96f9443fa 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -90,11 +90,14 @@ def __init__( f"Invalid seek mode ({seek_mode}). " f"Supported values are {', '.join(allowed_seek_modes)}." ) - if custom_frame_mappings and seek_mode != "exact": - raise ValueError( - "Custom frame mappings are only supported in 'exact' seek mode." - "While setting custom frame mappings, do not set `seek_mode`." - ) + if custom_frame_mappings: + if seek_mode != "exact": + raise ValueError( + "Custom frame mappings are only supported in 'exact' seek mode. " + "While setting custom frame mappings, do not set `seek_mode`." + ) + # Set seek mode to avoid exact mode scan + seek_mode = "custom_frame_mappings" custom_frame_mappings_data = ( read_custom_frame_mappings(custom_frame_mappings) if custom_frame_mappings is not None From 3e26ca7375df9e6f7695a5e36c163b660b650aa1 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 30 Jul 2025 15:17:45 -0400 Subject: [PATCH 03/22] Update test match string --- test/test_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ops.py b/test/test_ops.py index d2f2fd3b1..8a7e5935d 100644 --- a/test/test_ops.py +++ b/test/test_ops.py @@ -485,7 +485,7 @@ def test_seek_mode_custom_frame_mappings_fails(self): ) with pytest.raises( RuntimeError, - match="Please provide frame mappings when using custom_frame_mappings seek mode.", + match="Missing frame mappings when custom_frame_mappings seek mode is set.", ): add_video_stream(decoder, stream_index=0, custom_frame_mappings=None) From 17e7fc8615908ab90206cd23daf34f786b839522 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 1 Aug 2025 01:35:28 -0400 Subject: [PATCH 04/22] update json error message --- src/torchcodec/decoders/_video_decoder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 96f9443fa..4af756e56 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -118,7 +118,7 @@ def __init__( if isinstance(device, torch_device): device = str(device) - + core.add_video_stream( self._decoder, stream_index=stream_index, @@ -408,7 +408,7 @@ def read_custom_frame_mappings( except json.JSONDecodeError: raise ValueError( "Invalid custom frame mappings. " - "It should be a valid JSON string or a path to a JSON file." + "It should be a valid JSON string or a JSON file object." ) all_frames, is_key_frame, duration = zip( *[ From 4cd6fd8dee3a3296b79d9c525e860933dec33528 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 1 Aug 2025 17:26:51 -0400 Subject: [PATCH 05/22] lints --- src/torchcodec/decoders/_video_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 4af756e56..6709a8e20 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -118,7 +118,7 @@ def __init__( if isinstance(device, torch_device): device = str(device) - + core.add_video_stream( self._decoder, stream_index=stream_index, From ae369e31c114ef6aa69532a526050780044e2caf Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 1 Aug 2025 17:53:03 -0400 Subject: [PATCH 06/22] Fix type annotations for linter --- src/torchcodec/decoders/_video_decoder.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 6709a8e20..95bceec23 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -80,7 +80,7 @@ def __init__( dimension_order: Literal["NCHW", "NHWC"] = "NCHW", num_ffmpeg_threads: int = 1, device: Optional[Union[str, torch_device]] = "cpu", - seek_mode: Literal["exact", "approximate"] = "exact", + seek_mode: Literal["exact", "approximate", "custom_frame_mappings"] = "exact", custom_frame_mappings: Optional[Union[Path, str]] = None, ): torch._C._log_api_usage_once("torchcodec.decoders.VideoDecoder") @@ -91,9 +91,8 @@ def __init__( f"Supported values are {', '.join(allowed_seek_modes)}." ) if custom_frame_mappings: - if seek_mode != "exact": + if seek_mode not in ("exact", "custom_frame_mappings"): raise ValueError( - "Custom frame mappings are only supported in 'exact' seek mode. " "While setting custom frame mappings, do not set `seek_mode`." ) # Set seek mode to avoid exact mode scan @@ -398,7 +397,7 @@ def _get_and_validate_stream_metadata( def read_custom_frame_mappings( - custom_frame_mappings: Union[Path, str] + custom_frame_mappings: Union[bytes, bytearray, str] ) -> tuple[Tensor, Tensor, Tensor]: try: if hasattr(custom_frame_mappings, "read"): From 4857628145564cdd2c84656789e48108b5564802 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 1 Aug 2025 17:58:48 -0400 Subject: [PATCH 07/22] more annotation fixes --- src/torchcodec/decoders/_video_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 95bceec23..bb142c635 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -81,7 +81,7 @@ def __init__( num_ffmpeg_threads: int = 1, device: Optional[Union[str, torch_device]] = "cpu", seek_mode: Literal["exact", "approximate", "custom_frame_mappings"] = "exact", - custom_frame_mappings: Optional[Union[Path, str]] = None, + custom_frame_mappings: Union[bytes, bytearray, str] = None, ): torch._C._log_api_usage_once("torchcodec.decoders.VideoDecoder") allowed_seek_modes = ("exact", "approximate") From b00ba7ac79a051e5eca0ecddf985c0a24b2dca03 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 1 Aug 2025 18:06:47 -0400 Subject: [PATCH 08/22] type annotation --- src/torchcodec/decoders/_video_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index bb142c635..7c80401be 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -81,7 +81,7 @@ def __init__( num_ffmpeg_threads: int = 1, device: Optional[Union[str, torch_device]] = "cpu", seek_mode: Literal["exact", "approximate", "custom_frame_mappings"] = "exact", - custom_frame_mappings: Union[bytes, bytearray, str] = None, + custom_frame_mappings: Optional[Union[bytes, bytearray, str]] = None, ): torch._C._log_api_usage_once("torchcodec.decoders.VideoDecoder") allowed_seek_modes = ("exact", "approximate") From be9466b0a56bfe3eb0cc51327d8d99242fb7d596 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 20 Aug 2025 08:05:49 -0400 Subject: [PATCH 09/22] Update keys for older ffmpeg versions --- src/torchcodec/decoders/_video_decoder.py | 5 ++++- test/test_decoders.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 7c80401be..12cf6b271 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -409,9 +409,12 @@ def read_custom_frame_mappings( "Invalid custom frame mappings. " "It should be a valid JSON string or a JSON file object." ) + # These keys are prefixed with "pkt_" in ffmpeg 4 and ffmpeg 5 + pts_key = "pkt_pts" if "pts" not in input_data["frames"][0] else "pts" + duration_key = "pkt_duration" if "duration" not in input_data["frames"][0] else "duration" all_frames, is_key_frame, duration = zip( *[ - (float(frame["pts"]), frame["key_frame"], float(frame["duration"])) + (float(frame[pts_key]), frame["key_frame"], float(frame[duration_key])) for frame in input_data["frames"] ] ) diff --git a/test/test_decoders.py b/test/test_decoders.py index bf19bf1d8..6dfcf13cf 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1292,7 +1292,7 @@ def setup_frame_mappings(tmp_path: str, file: bool, stream_index: int): # Return the custom frame mappings as a JSON string return custom_frame_mappings - @pytest.mark.parametrize("device", cpu_and_cuda()) + @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize("stream_index", [0, 3]) @pytest.mark.parametrize( "method", @@ -1348,7 +1348,7 @@ def test_custom_frame_mappings(self, tmp_path, device, stream_index, method): ref_frame6 = H265_VIDEO.get_frame_data_by_index(5) assert_frames_equal(ref_frame6, decoder.get_frame_played_at(0.5).data) - @pytest.mark.parametrize("device", cpu_and_cuda()) + @pytest.mark.parametrize("device", all_supported_devices()) def test_custom_frame_mappings_init_fails(self, device): # Init fails if "approximate" seek mode is used with custom frame mappings with pytest.raises(ValueError, match="seek_mode"): From 40d120d3492a3dab8542e8f60edf8a7e50fd2f9c Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 20 Aug 2025 08:38:59 -0400 Subject: [PATCH 10/22] lint --- src/torchcodec/decoders/_video_decoder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 12cf6b271..1ae8037e0 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -411,7 +411,9 @@ def read_custom_frame_mappings( ) # These keys are prefixed with "pkt_" in ffmpeg 4 and ffmpeg 5 pts_key = "pkt_pts" if "pts" not in input_data["frames"][0] else "pts" - duration_key = "pkt_duration" if "duration" not in input_data["frames"][0] else "duration" + duration_key = ( + "pkt_duration" if "duration" not in input_data["frames"][0] else "duration" + ) all_frames, is_key_frame, duration = zip( *[ (float(frame[pts_key]), frame["key_frame"], float(frame[duration_key])) From c985db3d0fb2d5cbde79f3b219524c5aad7d32d4 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 20 Aug 2025 10:21:06 -0400 Subject: [PATCH 11/22] Test more failure cases --- test/test_decoders.py | 45 ++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/test/test_decoders.py b/test/test_decoders.py index 6dfcf13cf..ae2fa115e 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1301,7 +1301,9 @@ def setup_frame_mappings(tmp_path: str, file: bool, stream_index: int): partial(setup_frame_mappings, file=False), ), ) - def test_custom_frame_mappings(self, tmp_path, device, stream_index, method): + def test_custom_frame_mappings_json_and_bytes( + self, tmp_path, device, stream_index, method + ): custom_frame_mappings = method(tmp_path=tmp_path, stream_index=stream_index) # Optionally open the custom frame mappings file if it is a file path # or use a null context if it is a string. @@ -1340,25 +1342,46 @@ def test_custom_frame_mappings(self, tmp_path, device, stream_index, method): ), ) - decoder = VideoDecoder( - H265_VIDEO.path, - stream_index=0, - custom_frame_mappings=H265_VIDEO.generate_custom_frame_mappings(0), - ) - ref_frame6 = H265_VIDEO.get_frame_data_by_index(5) - assert_frames_equal(ref_frame6, decoder.get_frame_played_at(0.5).data) - @pytest.mark.parametrize("device", all_supported_devices()) - def test_custom_frame_mappings_init_fails(self, device): + def test_custom_frame_mappings_init_fails(self, tmp_path, device): # Init fails if "approximate" seek mode is used with custom frame mappings with pytest.raises(ValueError, match="seek_mode"): VideoDecoder( NASA_VIDEO.path, - stream_index=3, + stream_index=0, device=device, seek_mode="approximate", custom_frame_mappings=NASA_VIDEO.generate_custom_frame_mappings(3), ) + # Write an invalid JSON file for testing + invalid_json_path = tmp_path / "invalid_json" + with open(invalid_json_path, "w+") as f: + f.write("""'{"invalid": "json"'""") + # Init fails if invalid JSON bytes are passed in as custom frame mappings + with pytest.raises( + ValueError, + match="Invalid custom frame mappings. It should be a valid JSON string or a JSON file object.", + ): + with open(invalid_json_path, "r") as f: + VideoDecoder( + NASA_VIDEO.path, + stream_index=0, + device=device, + custom_frame_mappings=f, + ) + # Init fails if invalid JSON string is passed in as custom frame mappings + invalid_json_path = tmp_path / "invalid_json" + with pytest.raises( + ValueError, + match="Invalid custom frame mappings. It should be a valid JSON string or a JSON file object.", + ): + with open(invalid_json_path, "r") as f: + VideoDecoder( + NASA_VIDEO.path, + stream_index=0, + device=device, + custom_frame_mappings=f.read(), + ) class TestAudioDecoder: From 90241a0fd5e1761f559690de037ea20118a4ff8d Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Wed, 20 Aug 2025 10:26:53 -0400 Subject: [PATCH 12/22] Fix keys for ffmpeg4/5 in test_ops --- test/test_ops.py | 5 ----- test/utils.py | 9 +++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/test_ops.py b/test/test_ops.py index 8a7e5935d..f7d11a84d 100644 --- a/test/test_ops.py +++ b/test/test_ops.py @@ -44,7 +44,6 @@ from .utils import ( all_supported_devices, assert_frames_equal, - get_ffmpeg_major_version, NASA_AUDIO, NASA_AUDIO_MP3, NASA_VIDEO, @@ -505,10 +504,6 @@ def test_seek_mode_custom_frame_mappings_fails(self): decoder, stream_index=0, custom_frame_mappings=different_lengths ) - @pytest.mark.skipif( - get_ffmpeg_major_version() in (4, 5), - reason="ffprobe isn't accurate on ffmpeg 4 and 5", - ) @pytest.mark.parametrize("device", all_supported_devices()) def test_seek_mode_custom_frame_mappings(self, device): stream_index = 3 # custom_frame_index seek mode requires a stream index diff --git a/test/utils.py b/test/utils.py index ab5d0b77e..53db14a16 100644 --- a/test/utils.py +++ b/test/utils.py @@ -290,10 +290,15 @@ def generate_custom_frame_mappings(self, stream_index: int) -> str: def create_custom_frame_mappings(self, stream_index: int) -> None: result = json.loads(self.generate_custom_frame_mappings(stream_index)) - all_frames = torch.tensor([float(frame["pts"]) for frame in result["frames"]]) + # These keys are prefixed with "pkt_" in ffmpeg 4 and ffmpeg 5 + pts_key = "pkt_pts" if "pts" not in result["frames"][0] else "pts" + duration_key = ( + "pkt_duration" if "duration" not in result["frames"][0] else "duration" + ) + all_frames = torch.tensor([float(frame[pts_key]) for frame in result["frames"]]) is_key_frame = torch.tensor([frame["key_frame"] for frame in result["frames"]]) duration = torch.tensor( - [float(frame["duration"]) for frame in result["frames"]] + [float(frame[duration_key]) for frame in result["frames"]] ) assert ( len(all_frames) == len(is_key_frame) == len(duration) From 0b0037c60ed06e696022fc908594124b24bb6676 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Thu, 21 Aug 2025 09:39:29 -0400 Subject: [PATCH 13/22] Reuse read_custom_frame_mappings in utils.py --- test/utils.py | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/test/utils.py b/test/utils.py index 53db14a16..8e6864cb3 100644 --- a/test/utils.py +++ b/test/utils.py @@ -14,6 +14,7 @@ import torch from torchcodec._core import get_ffmpeg_library_versions +from torchcodec.decoders._video_decoder import read_custom_frame_mappings # Decorator for skipping CUDA tests when CUDA isn't available. The tests are @@ -267,7 +268,9 @@ def get_custom_frame_mappings( if stream_index is None: stream_index = self.default_stream_index if self._custom_frame_mappings_data.get(stream_index) is None: - self.create_custom_frame_mappings(stream_index) + self._custom_frame_mappings_data[stream_index] = read_custom_frame_mappings( + self.generate_custom_frame_mappings(stream_index) + ) return self._custom_frame_mappings_data[stream_index] def generate_custom_frame_mappings(self, stream_index: int) -> str: @@ -288,27 +291,6 @@ def generate_custom_frame_mappings(self, stream_index: int) -> str: ).stdout return result - def create_custom_frame_mappings(self, stream_index: int) -> None: - result = json.loads(self.generate_custom_frame_mappings(stream_index)) - # These keys are prefixed with "pkt_" in ffmpeg 4 and ffmpeg 5 - pts_key = "pkt_pts" if "pts" not in result["frames"][0] else "pts" - duration_key = ( - "pkt_duration" if "duration" not in result["frames"][0] else "duration" - ) - all_frames = torch.tensor([float(frame[pts_key]) for frame in result["frames"]]) - is_key_frame = torch.tensor([frame["key_frame"] for frame in result["frames"]]) - duration = torch.tensor( - [float(frame[duration_key]) for frame in result["frames"]] - ) - assert ( - len(all_frames) == len(is_key_frame) == len(duration) - ), "Mismatched lengths in frame index data" - self._custom_frame_mappings_data[stream_index] = ( - all_frames, - is_key_frame, - duration, - ) - @property def empty_pts_seconds(self) -> torch.Tensor: return torch.empty([0], dtype=torch.float64) From a88eab9d71cac7cbf087d86c8ce580477c771a02 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Thu, 21 Aug 2025 09:45:32 -0400 Subject: [PATCH 14/22] Update VideoDecoder init docstring, refactor frame_mappings logic --- src/torchcodec/decoders/_video_decoder.py | 50 ++++++++++++++++------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 1ae8037e0..fb56ebf75 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -63,7 +63,24 @@ class VideoDecoder: probably is. Default: "exact". Read more about this parameter in: :ref:`sphx_glr_generated_examples_decoding_approximate_mode.py` - + custom_frame_mappings (str, bytes, or file-like object, optional): + Mapping of frames to their metadata, typically generated via ffprobe. + This enables accurate frame seeking without requiring a full video scan + as in `exact` mode. Expected JSON format: + + .. code-block:: json + + { + "frames": [ + { + "pts": 0, + "duration": 1001, + "key_frame": 1 + ... # Other metadata fields can be included + }, + ... + ] + } Attributes: metadata (VideoStreamMetadata): Metadata of the video stream. @@ -81,27 +98,32 @@ def __init__( num_ffmpeg_threads: int = 1, device: Optional[Union[str, torch_device]] = "cpu", seek_mode: Literal["exact", "approximate", "custom_frame_mappings"] = "exact", - custom_frame_mappings: Optional[Union[bytes, bytearray, str]] = None, + custom_frame_mappings: Optional[ + Union[str, bytes, io.RawIOBase, io.BufferedReader] + ] = None, ): torch._C._log_api_usage_once("torchcodec.decoders.VideoDecoder") - allowed_seek_modes = ("exact", "approximate") + allowed_seek_modes = ("exact", "approximate", "custom_frame_mappings") if seek_mode not in allowed_seek_modes: raise ValueError( f"Invalid seek mode ({seek_mode}). " f"Supported values are {', '.join(allowed_seek_modes)}." ) - if custom_frame_mappings: - if seek_mode not in ("exact", "custom_frame_mappings"): - raise ValueError( - "While setting custom frame mappings, do not set `seek_mode`." - ) - # Set seek mode to avoid exact mode scan + + # Validate seek_mode and custom_frame_mappings are not mismatched + if custom_frame_mappings is not None and seek_mode == "approximate": + raise ValueError( + "custom_frame_mappings is incompatible with seek_mode='approximate'. " + "Use seek_mode='custom_frame_mappings' or leave it unspecified to automatically use custom frame mappings." + ) + + # Auto-select custom_frame_mappings seek_mode and process data when mappings are provided + custom_frame_mappings_data = None + if custom_frame_mappings is not None: seek_mode = "custom_frame_mappings" - custom_frame_mappings_data = ( - read_custom_frame_mappings(custom_frame_mappings) - if custom_frame_mappings is not None - else None - ) + custom_frame_mappings_data = read_custom_frame_mappings( + custom_frame_mappings + ) self._decoder = create_decoder(source=source, seek_mode=seek_mode) From 30467977037823ecc2950b9190356522bade5d3e Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Thu, 21 Aug 2025 09:46:46 -0400 Subject: [PATCH 15/22] Add docstring to read_custom_frame_mappings, refactor, error checking --- src/torchcodec/decoders/_video_decoder.py | 68 ++++++++++++++--------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index fb56ebf75..b9c2b3f96 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -421,35 +421,53 @@ def _get_and_validate_stream_metadata( def read_custom_frame_mappings( custom_frame_mappings: Union[bytes, bytearray, str] ) -> tuple[Tensor, Tensor, Tensor]: + """Parse custom frame mappings from JSON data and extract frame metadata. + + Args: + custom_frame_mappings: JSON data containing frame metadata, provided as: + - A JSON string (str, bytes, or bytearray) + - A file-like object with a read() method + + Returns: + A tuple of three tensors: + - all_frames (Tensor): Presentation timestamps (PTS) for each frame + - is_key_frame (Tensor): Boolean tensor indicating which frames are key frames + - duration (Tensor): Duration of each frame + """ try: - if hasattr(custom_frame_mappings, "read"): - input_data = json.load(custom_frame_mappings) - else: - input_data = json.loads(custom_frame_mappings) - except json.JSONDecodeError: + input_data = ( + json.load(custom_frame_mappings) + if hasattr(custom_frame_mappings, "read") + else json.loads(custom_frame_mappings) + ) + except json.JSONDecodeError as e: raise ValueError( - "Invalid custom frame mappings. " - "It should be a valid JSON string or a JSON file object." + f"Invalid custom frame mappings: {e}. It should be a valid JSON string or a file-like object." + ) from e + + if not input_data or "frames" not in input_data: + raise ValueError( + "Invalid custom frame mappings. The input is empty or missing the required 'frames' key." ) - # These keys are prefixed with "pkt_" in ffmpeg 4 and ffmpeg 5 - pts_key = "pkt_pts" if "pts" not in input_data["frames"][0] else "pts" - duration_key = ( - "pkt_duration" if "duration" not in input_data["frames"][0] else "duration" - ) - all_frames, is_key_frame, duration = zip( - *[ - (float(frame[pts_key]), frame["key_frame"], float(frame[duration_key])) - for frame in input_data["frames"] - ] + + first_frame = input_data["frames"][0] + pts_key = next((key for key in ("pts", "pkt_pts") if key in first_frame), None) + duration_key = next( + (key for key in ("duration", "pkt_duration") if key in first_frame), None ) - all_frames = Tensor(all_frames) - is_key_frame = Tensor(is_key_frame) - duration = Tensor(duration) + key_frame_present = "key_frame" in first_frame + + if not pts_key or not duration_key or not key_frame_present: + raise ValueError( + "Invalid custom frame mappings. The 'pts', 'duration', and 'key_frame' keys are required in the frame metadata." + ) + + frame_data = [ + (float(frame[pts_key]), frame["key_frame"], float(frame[duration_key])) + for frame in input_data["frames"] + ] + all_frames, is_key_frame, duration = map(Tensor, zip(*frame_data)) assert ( len(all_frames) == len(is_key_frame) == len(duration) ), "Mismatched lengths in frame index data" - return ( - all_frames, - is_key_frame, - duration, - ) + return all_frames, is_key_frame, duration From bbae18a3d746f45a311f482461a14470555c9740 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Thu, 21 Aug 2025 10:51:47 -0400 Subject: [PATCH 16/22] Test custom_frame_mappings errors --- test/test_decoders.py | 73 +++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/test/test_decoders.py b/test/test_decoders.py index ae2fa115e..454b3e0e5 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1343,45 +1343,56 @@ def test_custom_frame_mappings_json_and_bytes( ) @pytest.mark.parametrize("device", all_supported_devices()) - def test_custom_frame_mappings_init_fails(self, tmp_path, device): - # Init fails if "approximate" seek mode is used with custom frame mappings - with pytest.raises(ValueError, match="seek_mode"): + @pytest.mark.parametrize( + "custom_frame_mappings,expected_match", + [ + (NASA_VIDEO.generate_custom_frame_mappings(0), "seek_mode"), + ("{}", "The input is empty or missing the required 'frames' key."), + ( + '{"valid": "json"}', + "The input is empty or missing the required 'frames' key.", + ), + ( + '{"frames": [{"missing": "keys"}]}', + "The 'pts', 'duration', and 'key_frame' keys are required in the frame metadata.", + ), + ], + ) + def test_custom_frame_mappings_init_fails( + self, device, custom_frame_mappings, expected_match + ): + with pytest.raises(ValueError, match=expected_match): VideoDecoder( NASA_VIDEO.path, stream_index=0, device=device, - seek_mode="approximate", - custom_frame_mappings=NASA_VIDEO.generate_custom_frame_mappings(3), + custom_frame_mappings=custom_frame_mappings, + seek_mode=( + "approximate" + if expected_match == "seek_mode" + else "custom_frame_mappings" + ), ) - # Write an invalid JSON file for testing + + @pytest.mark.parametrize("device", all_supported_devices()) + def test_custom_frame_mappings_init_fails_invalid_json(self, tmp_path, device): invalid_json_path = tmp_path / "invalid_json" with open(invalid_json_path, "w+") as f: f.write("""'{"invalid": "json"'""") - # Init fails if invalid JSON bytes are passed in as custom frame mappings - with pytest.raises( - ValueError, - match="Invalid custom frame mappings. It should be a valid JSON string or a JSON file object.", - ): - with open(invalid_json_path, "r") as f: - VideoDecoder( - NASA_VIDEO.path, - stream_index=0, - device=device, - custom_frame_mappings=f, - ) - # Init fails if invalid JSON string is passed in as custom frame mappings - invalid_json_path = tmp_path / "invalid_json" - with pytest.raises( - ValueError, - match="Invalid custom frame mappings. It should be a valid JSON string or a JSON file object.", - ): - with open(invalid_json_path, "r") as f: - VideoDecoder( - NASA_VIDEO.path, - stream_index=0, - device=device, - custom_frame_mappings=f.read(), - ) + + # Test both file object and string + with open(invalid_json_path, "r") as file_obj: + for custom_frame_mappings in [ + file_obj, + file_obj.read(), + ]: + with pytest.raises(ValueError, match="Invalid custom frame mappings"): + VideoDecoder( + NASA_VIDEO.path, + stream_index=0, + device=device, + custom_frame_mappings=custom_frame_mappings, + ) class TestAudioDecoder: From 2a5f622b52719e2e441e8a6bb77e35e64a774e3b Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Thu, 21 Aug 2025 11:54:15 -0400 Subject: [PATCH 17/22] Fix type annotation --- src/torchcodec/decoders/_video_decoder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index b9c2b3f96..86b6ab2fa 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -419,13 +419,13 @@ def _get_and_validate_stream_metadata( def read_custom_frame_mappings( - custom_frame_mappings: Union[bytes, bytearray, str] + custom_frame_mappings: Union[str, bytes, io.RawIOBase, io.BufferedReader] ) -> tuple[Tensor, Tensor, Tensor]: """Parse custom frame mappings from JSON data and extract frame metadata. Args: custom_frame_mappings: JSON data containing frame metadata, provided as: - - A JSON string (str, bytes, or bytearray) + - A JSON string (str, bytes) - A file-like object with a read() method Returns: From 7acca9247fb0753ec9913f5f280b0997a0cdf1c0 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 22 Aug 2025 14:54:39 -0400 Subject: [PATCH 18/22] address comments --- src/torchcodec/decoders/_video_decoder.py | 17 ++++--- test/test_decoders.py | 54 ++++++++++------------- test/utils.py | 8 ++-- 3 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 86b6ab2fa..5ee556739 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -97,13 +97,13 @@ def __init__( dimension_order: Literal["NCHW", "NHWC"] = "NCHW", num_ffmpeg_threads: int = 1, device: Optional[Union[str, torch_device]] = "cpu", - seek_mode: Literal["exact", "approximate", "custom_frame_mappings"] = "exact", + seek_mode: Literal["exact", "approximate"] = "exact", custom_frame_mappings: Optional[ Union[str, bytes, io.RawIOBase, io.BufferedReader] ] = None, ): torch._C._log_api_usage_once("torchcodec.decoders.VideoDecoder") - allowed_seek_modes = ("exact", "approximate", "custom_frame_mappings") + allowed_seek_modes = ("exact", "approximate") if seek_mode not in allowed_seek_modes: raise ValueError( f"Invalid seek mode ({seek_mode}). " @@ -121,7 +121,7 @@ def __init__( custom_frame_mappings_data = None if custom_frame_mappings is not None: seek_mode = "custom_frame_mappings" - custom_frame_mappings_data = read_custom_frame_mappings( + custom_frame_mappings_data = _read_custom_frame_mappings( custom_frame_mappings ) @@ -418,7 +418,7 @@ def _get_and_validate_stream_metadata( ) -def read_custom_frame_mappings( +def _read_custom_frame_mappings( custom_frame_mappings: Union[str, bytes, io.RawIOBase, io.BufferedReader] ) -> tuple[Tensor, Tensor, Tensor]: """Parse custom frame mappings from JSON data and extract frame metadata. @@ -459,15 +459,14 @@ def read_custom_frame_mappings( if not pts_key or not duration_key or not key_frame_present: raise ValueError( - "Invalid custom frame mappings. The 'pts', 'duration', and 'key_frame' keys are required in the frame metadata." + "Invalid custom frame mappings. The 'pts'/'pkt_pts', 'duration'/'pkt_duration', and 'key_frame' keys are required in the frame metadata." ) frame_data = [ (float(frame[pts_key]), frame["key_frame"], float(frame[duration_key])) for frame in input_data["frames"] ] - all_frames, is_key_frame, duration = map(Tensor, zip(*frame_data)) - assert ( - len(all_frames) == len(is_key_frame) == len(duration) - ), "Mismatched lengths in frame index data" + all_frames, is_key_frame, duration = map(torch.tensor, zip(*frame_data)) + if not (len(all_frames) == len(is_key_frame) == len(duration)): + raise ValueError("Mismatched lengths in frame index data") return all_frames, is_key_frame, duration diff --git a/test/test_decoders.py b/test/test_decoders.py index 454b3e0e5..bf3482a11 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1280,7 +1280,7 @@ def test_10bit_videos_cpu(self, asset): decoder = VideoDecoder(asset.path) decoder.get_frame_at(10) - def setup_frame_mappings(tmp_path: str, file: bool, stream_index: int): + def setup_frame_mappings(tmp_path, file, stream_index): json_path = tmp_path / "custom_frame_mappings.json" custom_frame_mappings = NASA_VIDEO.generate_custom_frame_mappings(stream_index) if file: @@ -1318,29 +1318,25 @@ def test_custom_frame_mappings_json_and_bytes( device=device, custom_frame_mappings=custom_frame_mappings, ) - frame_0 = decoder.get_frame_at(0) - frame_5 = decoder.get_frame_at(5) - assert_frames_equal( - frame_0.data, - NASA_VIDEO.get_frame_data_by_index(0, stream_index=stream_index).to( - device - ), - ) - assert_frames_equal( - frame_5.data, - NASA_VIDEO.get_frame_data_by_index(5, stream_index=stream_index).to( - device - ), - ) - frames0_5 = decoder.get_frames_played_in_range( - frame_0.pts_seconds, frame_5.pts_seconds - ) - assert_frames_equal( - frames0_5.data, - NASA_VIDEO.get_frame_data_by_range(0, 5, stream_index=stream_index).to( - device - ), - ) + frame_0 = decoder.get_frame_at(0) + frame_5 = decoder.get_frame_at(5) + assert_frames_equal( + frame_0.data, + NASA_VIDEO.get_frame_data_by_index(0, stream_index=stream_index).to(device), + ) + assert_frames_equal( + frame_5.data, + NASA_VIDEO.get_frame_data_by_index(5, stream_index=stream_index).to(device), + ) + frames0_5 = decoder.get_frames_played_in_range( + frame_0.pts_seconds, frame_5.pts_seconds + ) + assert_frames_equal( + frames0_5.data, + NASA_VIDEO.get_frame_data_by_range(0, 5, stream_index=stream_index).to( + device + ), + ) @pytest.mark.parametrize("device", all_supported_devices()) @pytest.mark.parametrize( @@ -1354,7 +1350,7 @@ def test_custom_frame_mappings_json_and_bytes( ), ( '{"frames": [{"missing": "keys"}]}', - "The 'pts', 'duration', and 'key_frame' keys are required in the frame metadata.", + "keys are required in the frame metadata.", ), ], ) @@ -1367,18 +1363,14 @@ def test_custom_frame_mappings_init_fails( stream_index=0, device=device, custom_frame_mappings=custom_frame_mappings, - seek_mode=( - "approximate" - if expected_match == "seek_mode" - else "custom_frame_mappings" - ), + seek_mode=("approximate" if expected_match == "seek_mode" else "exact"), ) @pytest.mark.parametrize("device", all_supported_devices()) def test_custom_frame_mappings_init_fails_invalid_json(self, tmp_path, device): invalid_json_path = tmp_path / "invalid_json" with open(invalid_json_path, "w+") as f: - f.write("""'{"invalid": "json"'""") + f.write("invalid input") # Test both file object and string with open(invalid_json_path, "r") as file_obj: diff --git a/test/utils.py b/test/utils.py index 8e6864cb3..4cba27507 100644 --- a/test/utils.py +++ b/test/utils.py @@ -14,7 +14,7 @@ import torch from torchcodec._core import get_ffmpeg_library_versions -from torchcodec.decoders._video_decoder import read_custom_frame_mappings +from torchcodec.decoders._video_decoder import _read_custom_frame_mappings # Decorator for skipping CUDA tests when CUDA isn't available. The tests are @@ -268,8 +268,10 @@ def get_custom_frame_mappings( if stream_index is None: stream_index = self.default_stream_index if self._custom_frame_mappings_data.get(stream_index) is None: - self._custom_frame_mappings_data[stream_index] = read_custom_frame_mappings( - self.generate_custom_frame_mappings(stream_index) + self._custom_frame_mappings_data[stream_index] = ( + _read_custom_frame_mappings( + self.generate_custom_frame_mappings(stream_index) + ) ) return self._custom_frame_mappings_data[stream_index] From ea442e5b54a2b8ef88c48555722e79c3795b7c14 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 22 Aug 2025 15:57:38 -0400 Subject: [PATCH 19/22] ignore lint to not expose new seek_mode --- src/torchcodec/decoders/_video_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 5ee556739..b3fd34b5c 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -120,7 +120,7 @@ def __init__( # Auto-select custom_frame_mappings seek_mode and process data when mappings are provided custom_frame_mappings_data = None if custom_frame_mappings is not None: - seek_mode = "custom_frame_mappings" + seek_mode = "custom_frame_mappings" # type: ignore[assignment] custom_frame_mappings_data = _read_custom_frame_mappings( custom_frame_mappings ) From 81806dd201cf827bbd6051dfe242337cb00be6bd Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 22 Aug 2025 17:01:32 -0400 Subject: [PATCH 20/22] fix example json code-block --- src/torchcodec/decoders/_video_decoder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index b3fd34b5c..e17d8ff21 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -76,7 +76,6 @@ class VideoDecoder: "pts": 0, "duration": 1001, "key_frame": 1 - ... # Other metadata fields can be included }, ... ] From c634909eb180ac75c009d622c67cab4074a545a4 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Fri, 22 Aug 2025 18:17:24 -0400 Subject: [PATCH 21/22] remove elipsis from json --- src/torchcodec/decoders/_video_decoder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index e17d8ff21..8b037b85a 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -76,8 +76,7 @@ class VideoDecoder: "pts": 0, "duration": 1001, "key_frame": 1 - }, - ... + } ] } From 263637cb9ac063c35a0520d9194e45d5bddb1f86 Mon Sep 17 00:00:00 2001 From: Daniel Flores Date: Mon, 25 Aug 2025 09:31:53 -0400 Subject: [PATCH 22/22] Update docstring --- src/torchcodec/decoders/_video_decoder.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 8b037b85a..8c1152e8c 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -65,8 +65,9 @@ class VideoDecoder: :ref:`sphx_glr_generated_examples_decoding_approximate_mode.py` custom_frame_mappings (str, bytes, or file-like object, optional): Mapping of frames to their metadata, typically generated via ffprobe. - This enables accurate frame seeking without requiring a full video scan - as in `exact` mode. Expected JSON format: + This enables accurate frame seeking without requiring a full video scan. + Do not set seek_mode when custom_frame_mappings is provided. + Expected JSON format: .. code-block:: json @@ -80,6 +81,8 @@ class VideoDecoder: ] } + Alternative field names "pkt_pts" and "pkt_duration" are also supported. + Attributes: metadata (VideoStreamMetadata): Metadata of the video stream. stream_index (int): The stream index that this decoder is retrieving frames from. If a