Skip to content

Commit 1556872

Browse files
committed
Merge branch 'main' of github.com:meta-pytorch/torchcodec into worktree-stream-sync-events
2 parents 11f0da3 + 7da8f7d commit 1556872

20 files changed

Lines changed: 890 additions & 318 deletions

examples/decoding/blocks.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
.. code-block::
2323
2424
Demuxer -> PacketDecoder -> ColorConverter
25-
Packet DecodedFrame RGB Frame
25+
Packet RawFrame RGB Frame
2626
2727
The blocks are passive: they never create threads, and they release the GIL.
2828
You decide how they are composed, on which threads, and where to stop. Below
@@ -65,7 +65,8 @@
6565
#
6666
# ``PacketDecoder`` and ``ColorConverter`` both accept ``device="cuda"``:
6767
# decoding then runs on NVDEC and the color conversion on the GPU, and the
68-
# frames never leave the device. Demuxing always happens on the CPU.
68+
# frames never leave the device. Demuxing always happens on the CPU. Left
69+
# unspecified, ``device`` is the current default device.
6970
from torchcodec.decoders._blocks import ColorConverter, Demuxer, PacketDecoder
7071

7172
demuxer = Demuxer(video_path)
@@ -74,10 +75,10 @@
7475

7576
frames = []
7677
for packet in demuxer:
77-
for decoded_frame in packet_decoder.decode(packet):
78-
frames.append(color_converter.convert(decoded_frame))
79-
for decoded_frame in packet_decoder.drain():
80-
frames.append(color_converter.convert(decoded_frame))
78+
for raw_frame in packet_decoder.decode(packet):
79+
frames.append(color_converter.convert(raw_frame))
80+
for raw_frame in packet_decoder.drain():
81+
frames.append(color_converter.convert(raw_frame))
8182

8283
print(f"{len(frames)} frames, {frames[0].data.shape = }, "
8384
f"{frames[0].pts_seconds = }, {frames[0].data.device = }")
@@ -104,9 +105,9 @@ def decode(packet_decoder, packets):
104105
yield from packet_decoder.drain()
105106

106107

107-
def color_convert(color_converter, decoded_frames):
108-
for decoded_frame in decoded_frames:
109-
yield color_converter.convert(decoded_frame)
108+
def color_convert(color_converter, raw_frames):
109+
for raw_frame in raw_frames:
110+
yield color_converter.convert(raw_frame)
110111

111112

112113
def prefetch(upstream, buffer_size=8):
@@ -143,8 +144,8 @@ def convert_on_own_thread():
143144
demuxer = Demuxer(video_path)
144145
packet_decoder = PacketDecoder(demuxer, device=device)
145146
color_converter = ColorConverter(device=device)
146-
decoded_frames = prefetch(decode(packet_decoder, demux(demuxer)))
147-
return color_convert(color_converter, decoded_frames)
147+
raw_frames = prefetch(decode(packet_decoder, demux(demuxer)))
148+
return color_convert(color_converter, raw_frames)
148149

149150

150151
def demux_on_own_thread():
@@ -197,13 +198,12 @@ def demux_on_own_thread():
197198
# Raw frames
198199
# ----------
199200
#
200-
# Color conversion is optional. A ``DecodedFrame`` can hand out the decoder's
201-
# own planes as tensor views, with no copy and no conversion.
201+
# Color conversion is optional. A ``RawFrame`` can hand out the decoder's own
202+
# planes as tensor views, with no copy and no conversion.
202203
demuxer = Demuxer(video_path)
203204
packet_decoder = PacketDecoder(demuxer, device=device)
204-
decoded_frame = next(decode(packet_decoder, demux(demuxer)))
205+
raw_frame = next(decode(packet_decoder, demux(demuxer)))
205206

206-
raw_frame = decoded_frame.materialize()
207207
Y, U, V = raw_frame.planes
208208
print(f"{raw_frame.pix_fmt = }, {raw_frame.bit_depth = }, "
209209
f"{raw_frame.colorspace = }, {raw_frame.color_range = }")
@@ -215,8 +215,9 @@ def demux_on_own_thread():
215215
# views over a single plane. Writing through them is visible downstream.
216216
#
217217
# Being the decoder's own planes, they are also never rotated - a video whose
218-
# container asks for a rotation gives you the samples as they were encoded.
219-
# ``ColorConverter`` applies the rotation for you.
218+
# container asks for a rotation gives you the samples as they were encoded, and
219+
# ``raw_frame.rotation_degrees`` tells you what to apply. ``ColorConverter``
220+
# applies it for you.
220221
#
221222
# So we can do the color conversion ourselves. Here it's plain PyTorch ops -
222223
# it could just as well be a Triton or CUDA kernel, fused with whatever your
@@ -246,7 +247,7 @@ def upsample(plane):
246247

247248

248249
ours = yuv420_to_rgb(Y, U, V)
249-
reference = ColorConverter(device=device).convert(decoded_frame).data
250+
reference = ColorConverter(device=device).convert(raw_frame).data
250251
print(f"{ours.shape = }, mean abs diff vs ColorConverter: "
251252
f"{(ours.float() - reference.float()).abs().mean():.2f}")
252253

@@ -273,9 +274,8 @@ def upsample(plane):
273274

274275
hdr_demuxer = Demuxer(hdr_video_path)
275276
hdr_packet_decoder = PacketDecoder(hdr_demuxer, device=device)
276-
hdr_frame = next(decode(hdr_packet_decoder, demux(hdr_demuxer)))
277+
hdr_raw = next(decode(hdr_packet_decoder, demux(hdr_demuxer)))
277278

278-
hdr_raw = hdr_frame.materialize()
279279
hdr_Y = hdr_raw.planes[0]
280280
print(f"{hdr_raw.pix_fmt = }, {hdr_raw.bit_depth = }, "
281281
f"{hdr_raw.colorspace = }, {hdr_Y.dtype = }")

src/torchcodec/_core/BetaCudaDeviceInterface.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,8 @@ BetaCudaDeviceInterface::BetaCudaDeviceInterface(const StableDevice& device)
384384
STD_TORCH_CHECK(
385385
device_.type() == kStableCUDA, "Unsupported device: must be CUDA");
386386

387+
device_ = StableDevice(kStableCUDA, get_device_index(device_));
388+
387389
// Note: now that we have the CudaContextGuard, we might not need to do that
388390
// anymore. The comment says we need pytorch to create the context - maybe
389391
// that's true, but that's a very old comment now.

src/torchcodec/_core/ColorConverter.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@
1515

1616
namespace facebook::torchcodec {
1717

18+
namespace {
19+
// Only ever used to name a device in the error message below.
20+
std::string printable(const StableDevice& device) {
21+
std::string name = device_type_name(device.type());
22+
if (device.type() != kStableCPU && device.index() >= 0) {
23+
name += ":" + std::to_string(device.index());
24+
}
25+
return name;
26+
}
27+
} // namespace
28+
1829
ColorConverter::ColorConverter(
1930
const StableDevice& device,
2031
OutputDtypeConfig output_dtype_config)
@@ -23,6 +34,7 @@ ColorConverter::ColorConverter(
2334
STD_TORCH_CHECK(
2435
device_interface_ != nullptr,
2536
"Failed to create device interface. This should never happen, please report.");
37+
device_ = device_interface_->device(); // resolved, so we don't have to
2638
}
2739

2840
void ColorConverter::maybe_initialize_interface(OutputDtype output_dtype) {
@@ -44,7 +56,24 @@ void ColorConverter::maybe_initialize_interface(OutputDtype output_dtype) {
4456
initialized_output_dtype_ = output_dtype;
4557
}
4658

47-
torch::stable::Tensor ColorConverter::convert(const AVFrame& av_frame) {
59+
torch::stable::Tensor ColorConverter::convert(
60+
const AVFrame& av_frame,
61+
const StableDevice& frame_device) {
62+
// TODO_API_BREAKDOWN CC P2: OK, it's not fantastic that we have to pass the
63+
// frame's device. Especially given the related design TODO about whether the
64+
// RawFrame should carry that device field at all. Maybe it should, maybe it's
65+
// overkill. I think the main alternative is to retrieve the device from the
66+
// AVFrame, it's possible, but likely requires moving the
67+
// StandaloneFrameAttachedData to the public header.
68+
STD_TORCH_CHECK(
69+
frame_device == device_,
70+
"This ColorConverter is on ",
71+
printable(device_),
72+
" but the frame's samples are on ",
73+
printable(frame_device),
74+
". A ColorConverter only converts frames that are already on its own "
75+
"device: create one per device, or move the RGB output afterwards.");
76+
4877
OutputDtype output_dtype = resolve_output_dtype(
4978
output_dtype_config_, static_cast<AVPixelFormat>(av_frame.format));
5079
maybe_initialize_interface(output_dtype);

src/torchcodec/_core/ColorConverter.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include <memory>
1010
#include <optional>
11+
#include <string>
1112
#include <string_view>
1213

1314
#include "DeviceInterface.h"
@@ -23,7 +24,9 @@ class FORCE_PUBLIC_VISIBILITY ColorConverter {
2324
const StableDevice& device = StableDevice(kStableCPU),
2425
OutputDtypeConfig output_dtype_config = OutputDtypeConfig::UINT8);
2526

26-
torch::stable::Tensor convert(const AVFrame& av_frame);
27+
torch::stable::Tensor convert(
28+
const AVFrame& av_frame,
29+
const StableDevice& frame_device);
2730

2831
private:
2932
void maybe_initialize_interface(OutputDtype output_dtype);

src/torchcodec/_core/Demuxer.cpp

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,72 @@ Demuxer::Demuxer(
6060
STD_TORCH_CHECK(raw_context != nullptr, "Failed to allocate AVFormatContext");
6161
format_context_.reset(raw_context);
6262

63-
status = avformat_find_stream_info(format_context_.get(), nullptr);
63+
select_stream(stream_index);
64+
}
65+
66+
Demuxer::Demuxer(
67+
std::unique_ptr<AVIOContextHolder> avio_context_holder,
68+
std::optional<int> stream_index)
69+
: avio_context_holder_(std::move(avio_context_holder)) {
70+
set_ffmpeg_log_level();
71+
72+
STD_TORCH_CHECK(avio_context_holder_ != nullptr, "Context holder is null");
73+
74+
// FFmpeg takes a reference to the pointer in the call to open, so we can't
75+
// hand it a unique_ptr. That means we must free the context ourselves if the
76+
// open fails.
77+
AVFormatContext* raw_context = avformat_alloc_context();
78+
STD_TORCH_CHECK(raw_context != nullptr, "Failed to allocate AVFormatContext");
79+
raw_context->pb = avio_context_holder_->get_avio_context();
80+
81+
int status = avformat_open_input(&raw_context, nullptr, nullptr, nullptr);
82+
if (status != 0) {
83+
avformat_free_context(raw_context);
84+
STD_TORCH_CHECK(
85+
false,
86+
"Could not open input buffer: " +
87+
get_ffmpeg_error_string_from_error_code(status));
88+
}
89+
format_context_.reset(raw_context);
90+
91+
select_stream(stream_index);
92+
}
93+
94+
void Demuxer::validate_requested_stream(int stream_index) {
95+
int num_streams = static_cast<int>(format_context_->nb_streams);
96+
STD_TORCH_CHECK(
97+
stream_index >= 0 && stream_index < num_streams,
98+
"The stream index ",
99+
stream_index,
100+
" is not a valid stream. The file has ",
101+
num_streams,
102+
" streams, so the index must be in [0, ",
103+
num_streams - 1,
104+
"].");
105+
106+
AVMediaType media_type =
107+
format_context_->streams[stream_index]->codecpar->codec_type;
108+
const char* media_type_name = av_get_media_type_string(media_type);
109+
STD_TORCH_CHECK(
110+
media_type == AVMEDIA_TYPE_VIDEO,
111+
"The stream at index ",
112+
stream_index,
113+
" is not a video stream, it is of type '",
114+
media_type_name == nullptr ? "unknown" : media_type_name,
115+
"'. Only video streams can be demuxed.");
116+
}
117+
118+
void Demuxer::select_stream(std::optional<int> stream_index) {
119+
int status = avformat_find_stream_info(format_context_.get(), nullptr);
64120
STD_TORCH_CHECK(
65121
status >= 0,
66122
"Failed to find stream info: ",
67123
get_ffmpeg_error_string_from_error_code(status));
68124

125+
if (stream_index.has_value()) {
126+
validate_requested_stream(*stream_index);
127+
}
128+
69129
active_stream_index_ = av_find_best_stream(
70130
format_context_.get(),
71131
AVMEDIA_TYPE_VIDEO,
@@ -75,9 +135,8 @@ Demuxer::Demuxer(
75135
/*flags=*/0);
76136
STD_TORCH_CHECK(
77137
active_stream_index_ >= 0,
78-
"No valid video stream found in input file (requested index ",
79-
stream_index.value_or(-1),
80-
").");
138+
"No valid video stream found in input file. Only video streams are "
139+
"supported: audio streams cannot be demuxed.");
81140
stream_ = format_context_->streams[active_stream_index_];
82141

83142
// We only need packets from the active stream, so tell FFmpeg to discard the

src/torchcodec/_core/Demuxer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
#pragma once
88

9+
#include <memory>
910
#include <optional>
1011
#include <string>
1112

13+
#include "AVIOContextHolder.h"
1214
#include "FFMPEGCommon.h"
1315
#include "StableABICompat.h"
1416

@@ -36,6 +38,10 @@ class FORCE_PUBLIC_VISIBILITY Demuxer {
3638
const std::string& file_path,
3739
std::optional<int> stream_index = std::nullopt);
3840

41+
explicit Demuxer(
42+
std::unique_ptr<AVIOContextHolder> avio_context_holder,
43+
std::optional<int> stream_index = std::nullopt);
44+
3945
// Returns the next packet for the active stream as a freshly-allocated
4046
// packet, or a null packet at end of stream.
4147
UniqueAVPacket next_packet();
@@ -55,6 +61,12 @@ class FORCE_PUBLIC_VISIBILITY Demuxer {
5561
}
5662

5763
private:
64+
void validate_requested_stream(int stream_index);
65+
void select_stream(std::optional<int> stream_index);
66+
67+
// Declared before format_context_ so that it outlives it: the format context
68+
// reads through the AVIOContext this holds.
69+
std::unique_ptr<AVIOContextHolder> avio_context_holder_;
5870
UniqueDecodingAVFormatContext format_context_;
5971
int active_stream_index_ = -1;
6072
AVStream* stream_ = nullptr;

0 commit comments

Comments
 (0)