Skip to content

Add stream_index seek mode, read frame index and update metadata #764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
88cacb9
Add stream_index seek mode, read frame index and update metadata
Jul 10, 2025
4dfc581
add get_frame tests for new seek mode
Jul 10, 2025
ed3fdec
lints
Jul 10, 2025
6030f9e
replace union syntax with optional
Jul 10, 2025
1361c0d
remove color_conversion changes
Jul 10, 2025
5bb23c2
rename is_key_frame, remove readFrameIndex_, check tensor lengths are…
Jul 10, 2025
6573943
rename frame_index to custom_frame_mappings
Jul 10, 2025
3341dd8
lints
Jul 10, 2025
e914160
Use CustomFrameMappings struct
Jul 10, 2025
394ac70
use seek_mode keyword in test_ops
Jul 10, 2025
f13433c
Turn custom_frame_mappings_data into dict, use list comprehension
Jul 10, 2025
8971bd6
Remove c++ struct CustomFrameMappings
Jul 10, 2025
5d66ef1
underscore custom_frame_mappings_data field
Jul 14, 2025
4907e6d
set keyFrames in readCustomFrameMappingsUpdateMetadataAndIndex
Jul 14, 2025
d9cec94
Restore parameterized metadata_getter
Jul 14, 2025
72ae22d
Add frameMappings struct to wrap tensor tuple
Jul 14, 2025
34eb7b4
Extract sorting code to function
Jul 14, 2025
9a343b3
move FrameMappings struct to singlestreamdecoder.h
Jul 15, 2025
7728895
Rename variables in generate_custom_frame_mappings
Jul 15, 2025
dacd826
remove duplicate logic
Jul 15, 2025
f90718d
remove designated initializer
Jul 15, 2025
a24c79c
Check before accessing dict keys
Jul 15, 2025
ee6e867
add show_entries arg to ffprobe
Jul 15, 2025
011c8c5
skip test if ffmpeg 4 or 5, remove past debugging attempts
Jul 16, 2025
c5c5cd2
Use pytest skip in tests
Jul 16, 2025
4c6aadd
Reflect commented suggestions
Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 94 additions & 32 deletions src/torchcodec/_core/SingleStreamDecoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,45 @@ int SingleStreamDecoder::getBestStreamIndex(AVMediaType mediaType) {
// VIDEO METADATA QUERY API
// --------------------------------------------------------------------------

void SingleStreamDecoder::sortAllFrames() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a small comment to explain what this does

Suggested change
void SingleStreamDecoder::sortAllFrames() {
void SingleStreamDecoder::sortAllFrames() {
// Sort the allFrames and keyFrames vecs in each stream, and also sets
// additional fields of the FrameInfo entries like nextPts and frameIndex
// This is called at the end of a scan, or when setting a user-defined frame
// mapping.

// Sort the allFrames and keyFrames vecs in each stream, and also sets
// additional fields of the FrameInfo entries like nextPts and frameIndex
// This is called at the end of a scan, or when setting a user-defined frame
// mapping.
for (auto& [streamIndex, streamInfo] : streamInfos_) {
std::sort(
streamInfo.keyFrames.begin(),
streamInfo.keyFrames.end(),
[](const FrameInfo& frameInfo1, const FrameInfo& frameInfo2) {
return frameInfo1.pts < frameInfo2.pts;
});
std::sort(
streamInfo.allFrames.begin(),
streamInfo.allFrames.end(),
[](const FrameInfo& frameInfo1, const FrameInfo& frameInfo2) {
return frameInfo1.pts < frameInfo2.pts;
});

size_t keyFrameIndex = 0;
for (size_t i = 0; i < streamInfo.allFrames.size(); ++i) {
streamInfo.allFrames[i].frameIndex = i;
if (streamInfo.allFrames[i].isKeyFrame) {
TORCH_CHECK(
keyFrameIndex < streamInfo.keyFrames.size(),
"The allFrames vec claims it has MORE keyFrames than the keyFrames vec. There's a bug in torchcodec.");
streamInfo.keyFrames[keyFrameIndex].frameIndex = i;
++keyFrameIndex;
}
if (i + 1 < streamInfo.allFrames.size()) {
streamInfo.allFrames[i].nextPts = streamInfo.allFrames[i + 1].pts;
}
}
TORCH_CHECK(
keyFrameIndex == streamInfo.keyFrames.size(),
"The allFrames vec claims it has LESS keyFrames than the keyFrames vec. There's a bug in torchcodec.");
}
}

void SingleStreamDecoder::scanFileAndUpdateMetadataAndIndex() {
if (scannedAllStreams_) {
return;
Expand Down Expand Up @@ -283,40 +322,46 @@ void SingleStreamDecoder::scanFileAndUpdateMetadataAndIndex() {
getFFMPEGErrorStringFromErrorCode(status));

// Sort all frames by their pts.
for (auto& [streamIndex, streamInfo] : streamInfos_) {
std::sort(
streamInfo.keyFrames.begin(),
streamInfo.keyFrames.end(),
[](const FrameInfo& frameInfo1, const FrameInfo& frameInfo2) {
return frameInfo1.pts < frameInfo2.pts;
});
std::sort(
streamInfo.allFrames.begin(),
streamInfo.allFrames.end(),
[](const FrameInfo& frameInfo1, const FrameInfo& frameInfo2) {
return frameInfo1.pts < frameInfo2.pts;
});
sortAllFrames();
scannedAllStreams_ = true;
}

size_t keyFrameIndex = 0;
for (size_t i = 0; i < streamInfo.allFrames.size(); ++i) {
streamInfo.allFrames[i].frameIndex = i;
if (streamInfo.allFrames[i].isKeyFrame) {
TORCH_CHECK(
keyFrameIndex < streamInfo.keyFrames.size(),
"The allFrames vec claims it has MORE keyFrames than the keyFrames vec. There's a bug in torchcodec.");
streamInfo.keyFrames[keyFrameIndex].frameIndex = i;
++keyFrameIndex;
}
if (i + 1 < streamInfo.allFrames.size()) {
streamInfo.allFrames[i].nextPts = streamInfo.allFrames[i + 1].pts;
}
void SingleStreamDecoder::readCustomFrameMappingsUpdateMetadataAndIndex(
int streamIndex,
FrameMappings customFrameMappings) {
auto& all_frames = customFrameMappings.all_frames;
auto& is_key_frame = customFrameMappings.is_key_frame;
auto& duration = customFrameMappings.duration;
TORCH_CHECK(
all_frames.size(0) == is_key_frame.size(0) &&
is_key_frame.size(0) == duration.size(0),
"all_frames, is_key_frame, and duration from custom_frame_mappings were not same size.");

auto& streamMetadata = containerMetadata_.allStreamMetadata[streamIndex];

streamMetadata.beginStreamPtsFromContent = all_frames[0].item<int64_t>();
streamMetadata.endStreamPtsFromContent =
all_frames[-1].item<int64_t>() + duration[-1].item<int64_t>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL that tensors on the C++ side also support negative indices! I'm so used to that being not allowed in C++ arrays and standard containers that I initially thought this was undefined behavior!


auto avStream = formatContext_->streams[streamIndex];
streamMetadata.beginStreamPtsSecondsFromContent =
*streamMetadata.beginStreamPtsFromContent * av_q2d(avStream->time_base);

streamMetadata.endStreamPtsSecondsFromContent =
*streamMetadata.endStreamPtsFromContent * av_q2d(avStream->time_base);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably use the ptsToSeconds() function here, but it looks like we're also not using in the scanning function. And we can probably better define ptsToSeconds() to use av_q2d(). Let's address that elsewhere; created #770 to track.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just bumping this again as it may have been missed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address #770 after completing this change, so we can update + test all locations in one PR.

streamMetadata.numFramesFromContent = all_frames.size(0);
for (int64_t i = 0; i < all_frames.size(0); ++i) {
FrameInfo frameInfo;
frameInfo.pts = all_frames[i].item<int64_t>();
frameInfo.isKeyFrame = is_key_frame[i].item<bool>();
streamInfos_[streamIndex].allFrames.push_back(frameInfo);
if (frameInfo.isKeyFrame) {
streamInfos_[streamIndex].keyFrames.push_back(frameInfo);
}
TORCH_CHECK(
keyFrameIndex == streamInfo.keyFrames.size(),
"The allFrames vec claims it has LESS keyFrames than the keyFrames vec. There's a bug in torchcodec.");
}

scannedAllStreams_ = true;
// Sort all frames by their pts
sortAllFrames();
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize reading this that there is an additional design decision we'll have to make: whether we expect the index to be already sorted, or not.

In the existing scanFileAndUpdateMetadataAndIndex() function we are reading packets in order, and yet we are sorting them afterwards:

// Sort all frames by their pts.
for (auto& [streamIndex, streamInfo] : streamInfos_) {
std::sort(
streamInfo.keyFrames.begin(),
streamInfo.keyFrames.end(),
[](const FrameInfo& frameInfo1, const FrameInfo& frameInfo2) {
return frameInfo1.pts < frameInfo2.pts;
});
std::sort(
streamInfo.allFrames.begin(),
streamInfo.allFrames.end(),
[](const FrameInfo& frameInfo1, const FrameInfo& frameInfo2) {
return frameInfo1.pts < frameInfo2.pts;
});

I suspect that frame mappings coming from ffprobe won't be ordered in general. I think we have the following options:

  • expect the input mapping to be sorted - that may not be a great UX
  • sort the mapping in Python - this is kinda what this PR is doing, by sorting the mappings in the tests - but we should remove that and have that logic within the code, not the tests
  • sort the mappings in C++

I think the simpler, safer choice is to sort in C++ and rely on the same sorting logic that we have in scanFileAndUpdateMetadataAndIndex(). Curious what your thoughts are @Dan-Flores @scotts ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on sorting on the C++ side; we can do it quickly on the C++ side, and it's much nicer to users. We should extract the existing logic out to a utility function called only in this cpp file, and call it in both places.

I think we sort in the scan function because I think it's possible for the actual packets to be not in PTS order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! Sorting on the C++ side makes sense, I've updated the PR to reuse the sorting code from the scan function.

ContainerMetadata SingleStreamDecoder::getContainerMetadata() const {
Expand Down Expand Up @@ -431,7 +476,8 @@ void SingleStreamDecoder::addStream(

void SingleStreamDecoder::addVideoStream(
int streamIndex,
const VideoStreamOptions& videoStreamOptions) {
const VideoStreamOptions& videoStreamOptions,
std::optional<FrameMappings> customFrameMappings) {
addStream(
streamIndex,
AVMEDIA_TYPE_VIDEO,
Expand All @@ -456,6 +502,14 @@ void SingleStreamDecoder::addVideoStream(
streamMetadata.height = streamInfo.codecContext->height;
streamMetadata.sampleAspectRatio =
streamInfo.codecContext->sample_aspect_ratio;

if (seekMode_ == SeekMode::custom_frame_mappings) {
TORCH_CHECK(
customFrameMappings.has_value(),
"Please provide frame mappings when using custom_frame_mappings seek mode.");
readCustomFrameMappingsUpdateMetadataAndIndex(
streamIndex, customFrameMappings.value());
}
}

void SingleStreamDecoder::addAudioStream(
Expand Down Expand Up @@ -1407,6 +1461,7 @@ int SingleStreamDecoder::getKeyFrameIndexForPtsUsingScannedIndex(
int64_t SingleStreamDecoder::secondsToIndexLowerBound(double seconds) {
auto& streamInfo = streamInfos_[activeStreamIndex_];
switch (seekMode_) {
case SeekMode::custom_frame_mappings:
case SeekMode::exact: {
auto frame = std::lower_bound(
streamInfo.allFrames.begin(),
Expand Down Expand Up @@ -1434,6 +1489,7 @@ int64_t SingleStreamDecoder::secondsToIndexLowerBound(double seconds) {
int64_t SingleStreamDecoder::secondsToIndexUpperBound(double seconds) {
auto& streamInfo = streamInfos_[activeStreamIndex_];
switch (seekMode_) {
case SeekMode::custom_frame_mappings:
case SeekMode::exact: {
auto frame = std::upper_bound(
streamInfo.allFrames.begin(),
Expand Down Expand Up @@ -1461,6 +1517,7 @@ int64_t SingleStreamDecoder::secondsToIndexUpperBound(double seconds) {
int64_t SingleStreamDecoder::getPts(int64_t frameIndex) {
auto& streamInfo = streamInfos_[activeStreamIndex_];
switch (seekMode_) {
case SeekMode::custom_frame_mappings:
case SeekMode::exact:
return streamInfo.allFrames[frameIndex].pts;
case SeekMode::approximate: {
Expand All @@ -1485,6 +1542,7 @@ int64_t SingleStreamDecoder::getPts(int64_t frameIndex) {
std::optional<int64_t> SingleStreamDecoder::getNumFrames(
const StreamMetadata& streamMetadata) {
switch (seekMode_) {
case SeekMode::custom_frame_mappings:
case SeekMode::exact:
return streamMetadata.numFramesFromContent.value();
case SeekMode::approximate: {
Expand All @@ -1498,6 +1556,7 @@ std::optional<int64_t> SingleStreamDecoder::getNumFrames(
double SingleStreamDecoder::getMinSeconds(
const StreamMetadata& streamMetadata) {
switch (seekMode_) {
case SeekMode::custom_frame_mappings:
case SeekMode::exact:
return streamMetadata.beginStreamPtsSecondsFromContent.value();
case SeekMode::approximate:
Expand All @@ -1510,6 +1569,7 @@ double SingleStreamDecoder::getMinSeconds(
std::optional<double> SingleStreamDecoder::getMaxSeconds(
const StreamMetadata& streamMetadata) {
switch (seekMode_) {
case SeekMode::custom_frame_mappings:
case SeekMode::exact:
return streamMetadata.endStreamPtsSecondsFromContent.value();
case SeekMode::approximate: {
Expand Down Expand Up @@ -1645,6 +1705,8 @@ SingleStreamDecoder::SeekMode seekModeFromString(std::string_view seekMode) {
return SingleStreamDecoder::SeekMode::exact;
} else if (seekMode == "approximate") {
return SingleStreamDecoder::SeekMode::approximate;
} else if (seekMode == "custom_frame_mappings") {
return SingleStreamDecoder::SeekMode::custom_frame_mappings;
} else {
TORCH_CHECK(false, "Invalid seek mode: " + std::string(seekMode));
}
Expand Down
29 changes: 27 additions & 2 deletions src/torchcodec/_core/SingleStreamDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class SingleStreamDecoder {
// CONSTRUCTION API
// --------------------------------------------------------------------------

enum class SeekMode { exact, approximate };
enum class SeekMode { exact, approximate, custom_frame_mappings };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we'll want to publicly expose a new seek_mode in Python, but I think for the C++ side this a reasonable approach. @scotts curious if you have any opinion on this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Right now, the SingleStreamDecoder's seekMode_ is what we use to determine if we should do a file scan or not:

if (seekMode_ == SeekMode::exact) {
scanFileAndUpdateMetadataAndIndex();
}

Since the point of this feature is to avoid a file scan while still maintaining seek accuracy, I think it makes sense for it to be a new seek mode on the C++ side. We can figure out what to do with the public API later.


// Creates a SingleStreamDecoder from the video at videoFilePath.
explicit SingleStreamDecoder(
Expand All @@ -53,20 +53,38 @@ class SingleStreamDecoder {
// the allFrames and keyFrames vectors.
void scanFileAndUpdateMetadataAndIndex();

// Sorts the keyFrames and allFrames vectors in each StreamInfo by pts.
void sortAllFrames();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just bumping #764 (comment) again, which may have been missed:

  • we should document what are the expected length, dtype, and associated semantic of each tensor.
  • I'd also recommend relying on a new FrameMappings struct instead of a 3-tuple.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate on defining a new FrameMappings struct: I think of the code in custom_ops.cpp as the bridge layer between the C++ logic and the Python logic. So that's the layer where we would turn a tuple of tensors into a struct. That way, in the C++ logic, we're (as much as possible) operating on proper types with meaningful field names.

As a point of comparison, we do something similar with batched output. SingleStreamDecoder returns a FrameBatchOutput struct, and the code in custom_ops.cpp turns that into a tuple of tensors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reference. I added FrameMappings struct to Frame.h and utilized it on the C++ side, please let me know if the design pattern needs any adjustments.

// Returns the metadata for the container.
ContainerMetadata getContainerMetadata() const;

// Returns the key frame indices as a tensor. The tensor is 1D and contains
// int64 values, where each value is the frame index for a key frame.
torch::Tensor getKeyFrameIndices();

// FrameMappings is used for the custom_frame_mappings seek mode to store
// metadata of frames in a stream. The size of all tensors in this struct must
// match.

// --------------------------------------------------------------------------
// ADDING STREAMS API
// --------------------------------------------------------------------------
struct FrameMappings {
// 1D tensor of int64, each value is the PTS of a frame in timebase units.
torch::Tensor all_frames;
// 1D tensor of bool, each value indicates if the corresponding frame in
// all_frames is a key frame.
torch::Tensor is_key_frame;
// 1D tensor of int64, each value is the duration of the corresponding frame
// in all_frames in timebase units.
torch::Tensor duration;
};

void addVideoStream(
int streamIndex,
const VideoStreamOptions& videoStreamOptions = VideoStreamOptions());
const VideoStreamOptions& videoStreamOptions = VideoStreamOptions(),
std::optional<FrameMappings> customFrameMappings = std::nullopt);
void addAudioStream(
int streamIndex,
const AudioStreamOptions& audioStreamOptions = AudioStreamOptions());
Expand Down Expand Up @@ -226,6 +244,13 @@ class SingleStreamDecoder {
// --------------------------------------------------------------------------

void initializeDecoder();

// Reads the user provided frame index and updates each StreamInfo's index,
// i.e. the allFrames and keyFrames vectors, and
// endStreamPtsSecondsFromContent
void readCustomFrameMappingsUpdateMetadataAndIndex(
int streamIndex,
FrameMappings customFrameMappings);
// --------------------------------------------------------------------------
// DECODING APIS AND RELATED UTILS
// --------------------------------------------------------------------------
Expand Down
29 changes: 23 additions & 6 deletions src/torchcodec/_core/custom_ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ TORCH_LIBRARY(torchcodec_ns, m) {
"create_from_tensor(Tensor video_tensor, str? seek_mode=None) -> Tensor");
m.def("_convert_to_tensor(int decoder_ptr) -> Tensor");
m.def(
"_add_video_stream(Tensor(a!) decoder, *, int? width=None, int? height=None, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str? device=None, str? color_conversion_library=None) -> ()");
"_add_video_stream(Tensor(a!) decoder, *, int? width=None, int? height=None, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str? device=None, (Tensor, Tensor, Tensor)? custom_frame_mappings=None, str? color_conversion_library=None) -> ()");
m.def(
"add_video_stream(Tensor(a!) decoder, *, int? width=None, int? height=None, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str? device=None) -> ()");
"add_video_stream(Tensor(a!) decoder, *, int? width=None, int? height=None, int? num_threads=None, str? dimension_order=None, int? stream_index=None, str? device=None, (Tensor, Tensor, Tensor)? custom_frame_mappings=None) -> ()");
m.def(
"add_audio_stream(Tensor(a!) decoder, *, int? stream_index=None, int? sample_rate=None, int? num_channels=None) -> ()");
m.def("seek_to_pts(Tensor(a!) decoder, float seconds) -> ()");
Expand Down Expand Up @@ -105,6 +105,14 @@ OpsFrameOutput makeOpsFrameOutput(FrameOutput& frame) {
torch::tensor(frame.durationSeconds, torch::dtype(torch::kFloat64)));
}

SingleStreamDecoder::FrameMappings makeFrameMappings(
std::tuple<at::Tensor, at::Tensor, at::Tensor> custom_frame_mappings) {
return SingleStreamDecoder::FrameMappings{
std::get<0>(custom_frame_mappings),
std::get<1>(custom_frame_mappings),
std::get<2>(custom_frame_mappings)};
}

// All elements of this tuple are tensors of the same leading dimension. The
// tuple represents the frames for N total frames, where N is the dimension of
// each stacked tensor. The elments are:
Expand Down Expand Up @@ -223,6 +231,8 @@ void _add_video_stream(
std::optional<std::string_view> dimension_order = std::nullopt,
std::optional<int64_t> stream_index = std::nullopt,
std::optional<std::string_view> device = std::nullopt,
std::optional<std::tuple<at::Tensor, at::Tensor, at::Tensor>>
custom_frame_mappings = std::nullopt,
std::optional<std::string_view> color_conversion_library = std::nullopt) {
VideoStreamOptions videoStreamOptions;
videoStreamOptions.width = width;
Expand Down Expand Up @@ -253,9 +263,13 @@ void _add_video_stream(
if (device.has_value()) {
videoStreamOptions.device = createTorchDevice(std::string(device.value()));
}

std::optional<SingleStreamDecoder::FrameMappings> converted_mappings =
custom_frame_mappings.has_value()
? std::make_optional(makeFrameMappings(custom_frame_mappings.value()))
: std::nullopt;
auto videoDecoder = unwrapTensorToGetDecoder(decoder);
videoDecoder->addVideoStream(stream_index.value_or(-1), videoStreamOptions);
videoDecoder->addVideoStream(
stream_index.value_or(-1), videoStreamOptions, converted_mappings);
}

// Add a new video stream at `stream_index` using the provided options.
Expand All @@ -266,15 +280,18 @@ void add_video_stream(
std::optional<int64_t> num_threads = std::nullopt,
std::optional<std::string_view> dimension_order = std::nullopt,
std::optional<int64_t> stream_index = std::nullopt,
std::optional<std::string_view> device = std::nullopt) {
std::optional<std::string_view> device = std::nullopt,
std::optional<std::tuple<at::Tensor, at::Tensor, at::Tensor>>
custom_frame_mappings = std::nullopt) {
_add_video_stream(
decoder,
width,
height,
num_threads,
dimension_order,
stream_index,
device);
device,
custom_frame_mappings);
}

void add_audio_stream(
Expand Down
6 changes: 6 additions & 0 deletions src/torchcodec/_core/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ def _add_video_stream_abstract(
dimension_order: Optional[str] = None,
stream_index: Optional[int] = None,
device: Optional[str] = None,
custom_frame_mappings: Optional[
tuple[torch.Tensor, torch.Tensor, torch.Tensor]
] = None,
color_conversion_library: Optional[str] = None,
) -> None:
return
Expand All @@ -220,6 +223,9 @@ def add_video_stream_abstract(
dimension_order: Optional[str] = None,
stream_index: Optional[int] = None,
device: Optional[str] = None,
custom_frame_mappings: Optional[
tuple[torch.Tensor, torch.Tensor, torch.Tensor]
] = None,
) -> None:
return

Expand Down
Loading
Loading