Skip to content

Commit 3be0df9

Browse files
authored
Implement seeking in blocks (#1652)
1 parent 031201a commit 3be0df9

15 files changed

Lines changed: 617 additions & 85 deletions

benchmarks/bench_blocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _demux(demuxer):
6767
def _decode(decoder, packets):
6868
for packet in packets:
6969
yield from decoder.decode(packet)
70-
yield from decoder.flush()
70+
yield from decoder.drain()
7171

7272

7373
def _convert(converter, frames):

examples/decoding/blocks.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
# ----------------
6161
#
6262
# A pipeline is just a loop. The decoder may need more than one packet before
63-
# it can output a frame, and it buffers a few frames that ``flush()`` returns
63+
# it can output a frame, and it buffers a few frames that ``drain()`` returns
6464
# at the end.
6565
#
6666
# ``PacketDecoder`` and ``ColorConverter`` both accept ``device="cuda"``:
@@ -76,7 +76,7 @@
7676
for packet in demuxer:
7777
for decoded_frame in packet_decoder.decode(packet):
7878
frames.append(color_converter.convert(decoded_frame))
79-
for decoded_frame in packet_decoder.flush():
79+
for decoded_frame in packet_decoder.drain():
8080
frames.append(color_converter.convert(decoded_frame))
8181

8282
print(f"{len(frames)} frames, {frames[0].data.shape = }, "
@@ -101,7 +101,7 @@ def demux(demuxer):
101101
def decode(packet_decoder, packets):
102102
for packet in packets:
103103
yield from packet_decoder.decode(packet)
104-
yield from packet_decoder.flush()
104+
yield from packet_decoder.drain()
105105

106106

107107
def color_convert(color_converter, decoded_frames):
@@ -168,6 +168,31 @@ def demux_on_own_thread():
168168
# CPU while color-converting on the GPU, or feeding frames into your own
169169
# pre-fetching data loader.
170170

171+
# %%
172+
# Seeking
173+
# -------
174+
#
175+
# ``Demuxer.seek()`` moves the demuxer to a timestamp. A decoder can only start
176+
# on a keyframe, so the seek lands on the keyframe at or before the target, and
177+
# the first frames that come out usually precede it: keep decoding forward and
178+
# drop them until you reach the timestamp you asked for.
179+
#
180+
# The seek also invalidates the frames the decoder is holding on to, so the
181+
# ``PacketDecoder`` must be ``reset()``.
182+
demuxer = Demuxer(video_path)
183+
packet_decoder = PacketDecoder(demuxer, device=device)
184+
color_converter = ColorConverter(device=device)
185+
186+
seconds = 2.5
187+
demuxer.seek(seconds)
188+
packet_decoder.reset()
189+
190+
frames = color_convert(color_converter, decode(packet_decoder, demux(demuxer)))
191+
landed_on = next(frames)
192+
target = next(frame for frame in frames if frame.pts_seconds >= seconds)
193+
print(f"asked for {seconds}s, landed on {landed_on.pts_seconds:.3f}s, "
194+
f"target frame at {target.pts_seconds:.3f}s")
195+
171196
# %%
172197
# Raw frames
173198
# ----------

src/torchcodec/_core/Demuxer.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,32 @@
55
// LICENSE file in the root directory of this source tree.
66

77
#include "Demuxer.h"
8+
9+
#include <sstream>
10+
811
#include "StableABICompat.h"
912

1013
namespace facebook::torchcodec {
1114

15+
// FFmpeg reports "this seek cannot be performed" as a bare -1, i.e. EPERM,
16+
// which renders as the very misleading "Operation not permitted". It covers
17+
// both a target that the demuxer can't reach and a demuxer with no seeking
18+
// support whatsoever.
19+
std::string get_seek_error_message(
20+
const AVFormatContext* format_context,
21+
int64_t desired_pts,
22+
int status) {
23+
std::stringstream ss;
24+
ss << "Could not seek file to pts=" << desired_pts << ": "
25+
<< get_ffmpeg_error_string_from_error_code(status) << ".";
26+
if (status == AVERROR(EPERM)) {
27+
ss << " This is either because that timestamp is out of range, or because"
28+
<< " the '" << format_context->iformat->name << "' format does not"
29+
<< " support seeking.";
30+
}
31+
return ss.str();
32+
}
33+
1234
int read_next_packet(
1335
AVFormatContext* format_context,
1436
int active_stream_index,
@@ -68,6 +90,21 @@ Demuxer::Demuxer(
6890
}
6991
}
7092

93+
void Demuxer::seek(double seconds) {
94+
int64_t desired_pts = seconds_to_closest_pts(seconds, stream_->time_base);
95+
96+
int status = avformat_seek_file(
97+
format_context_.get(),
98+
active_stream_index_,
99+
INT64_MIN,
100+
desired_pts,
101+
desired_pts,
102+
0);
103+
STD_TORCH_CHECK(
104+
status >= 0,
105+
get_seek_error_message(format_context_.get(), desired_pts, status));
106+
}
107+
71108
UniqueAVPacket Demuxer::next_packet() {
72109
// TODO_API_BREAKDOWN CC P2: Not a fan of the ReferenceAVPacket / AutoAVPacket
73110
// / UniqueAVPacket dance here. Can we simplify?

src/torchcodec/_core/Demuxer.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ int read_next_packet(
2323
int active_stream_index,
2424
ReferenceAVPacket& packet);
2525

26+
std::string get_seek_error_message(
27+
const AVFormatContext* format_context,
28+
int64_t desired_pts,
29+
int status);
30+
2631
// Demux building block: owns an AVFormatContext, selects one video stream, and
2732
// yields its (compressed) packets. Does no decoding. Not thread-safe.
2833
class FORCE_PUBLIC_VISIBILITY Demuxer {
@@ -35,6 +40,8 @@ class FORCE_PUBLIC_VISIBILITY Demuxer {
3540
// packet, or a null packet at end of stream.
3641
UniqueAVPacket next_packet();
3742

43+
void seek(double seconds);
44+
3845
AVStream* active_stream() const {
3946
return stream_;
4047
}

src/torchcodec/_core/PacketDecoder.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ PacketDecoder::PacketDecoder(
6565

6666
AVStream* stream = demuxer.active_stream();
6767
time_base_ = stream->time_base;
68+
is_mpeg_ps_ =
69+
std::string_view(demuxer.format_context()->iformat->name) == "mpeg";
6870
if (const int32_t* matrix = get_display_matrix_from_stream(stream)) {
6971
display_matrix_.emplace();
7072
std::copy(
@@ -100,13 +102,33 @@ int PacketDecoder::send_packet(AVPacket* packet) {
100102
ReferenceAVPacket ref(auto_packet);
101103
int status = av_packet_ref(ref.get(), packet);
102104
STD_TORCH_CHECK(status >= AVSUCCESS, "av_packet_ref failed");
103-
return device_interface_->send_packet(ref);
105+
106+
status = device_interface_->send_packet(ref);
107+
108+
if (status == AVERROR_INVALIDDATA && packet_data_may_be_misaligned_) {
109+
// Seeking in an MPEG program stream lands on a container-level byte offset,
110+
// so the parser resumes mid-frame and the packets it rebuilds are garbage
111+
// until it resyncs. Report those as consumed rather than as a corrupt file:
112+
// dropping them is exactly what resyncing means.
113+
return AVSUCCESS;
114+
}
115+
if (status >= AVSUCCESS) {
116+
// The decoder accepted a packet, so we're aligned again: from now on
117+
// invalid data means the file is corrupt, and we want to report it.
118+
packet_data_may_be_misaligned_ = false;
119+
}
120+
return status;
104121
}
105122

106123
int PacketDecoder::send_eof() {
107124
return device_interface_->send_eof_packet();
108125
}
109126

127+
void PacketDecoder::reset() {
128+
device_interface_->flush();
129+
packet_data_may_be_misaligned_ = is_mpeg_ps_;
130+
}
131+
110132
int PacketDecoder::receive_frame(UniqueAVFrame& av_frame) {
111133
int status = device_interface_->receive_frame(av_frame);
112134
if (status == AVSUCCESS) {

src/torchcodec/_core/PacketDecoder.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ class FORCE_PUBLIC_VISIBILITY PacketDecoder {
4545
// Pull one frame. Returns AVSUCCESS with `av_frame` filled, AVERROR(EAGAIN)
4646
// if more input is needed, AVERROR_EOF at end, or a negative error code.
4747
int receive_frame(UniqueAVFrame& av_frame);
48+
// Drop the codec's buffered state (reference frames, in-flight frames) and
49+
// start over. Needed after the demuxer seeked, and after send_eof(), which
50+
// otherwise leaves the codec permanently in its drained state.
51+
// This is called 'reset()' and not 'flush()', because this is publicly
52+
// exposed flush() is slightly ambiguous and could mean 'flush the frames out
53+
// of the decoder' rather than meaning 'flush the decoder internal state'.
54+
void reset();
4855

4956
std::optional<torch::stable::Tensor> get_frame_storage(
5057
const AVFrame& av_frame) const {
@@ -69,6 +76,10 @@ class FORCE_PUBLIC_VISIBILITY PacketDecoder {
6976
// value: we're only handed the Demuxer at construction and it may well be
7077
// gone by the time we decode.
7178
std::optional<std::array<int32_t, 9>> display_matrix_;
79+
// The MPEG-PS demuxer doesn't return proper packets just after a seek, so the
80+
// first ones we're fed may not be decodable. See send_packet().
81+
bool is_mpeg_ps_ = false;
82+
bool packet_data_may_be_misaligned_ = false;
7283
};
7384

7485
// A decoded frame's own samples, before any color conversion.

src/torchcodec/_core/SingleStreamDecoder.cpp

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,6 @@ extern "C" {
2525

2626
namespace facebook::torchcodec {
2727

28-
namespace {
29-
30-
// FFmpeg reports "this seek cannot be performed" as a bare -1, i.e. EPERM,
31-
// which renders as the very misleading "Operation not permitted". It covers
32-
// both a target that the demuxer can't reach and a demuxer with no seeking
33-
// support whatsoever.
34-
std::string get_seek_error_message(
35-
const AVFormatContext* format_context,
36-
int64_t desired_pts,
37-
int status) {
38-
std::stringstream ss;
39-
ss << "Could not seek file to pts=" << desired_pts << ": "
40-
<< get_ffmpeg_error_string_from_error_code(status) << ".";
41-
if (status == AVERROR(EPERM)) {
42-
ss << " This is either because that timestamp is out of range, or because"
43-
<< " the '" << format_context->iformat->name << "' format does not"
44-
<< " support seeking.";
45-
}
46-
return ss.str();
47-
}
48-
49-
} // namespace
50-
5128
// --------------------------------------------------------------------------
5229
// CONSTRUCTORS, INITIALIZATION, DESTRUCTORS
5330
// --------------------------------------------------------------------------

src/torchcodec/_core/_ffmpeg_op_names.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
"get_json_metadata",
3232
"_blocks_create_demuxer",
3333
"_blocks_demuxer_next_packet",
34+
"_blocks_demuxer_seek",
3435
"_blocks_create_packet_decoder",
3536
"_blocks_packet_decoder_send_packet",
3637
"_blocks_packet_decoder_send_eof",
38+
"_blocks_packet_decoder_reset",
3739
"_blocks_packet_decoder_receive_frame",
3840
"_blocks_create_color_converter",
3941
"_blocks_convert_frame",

src/torchcodec/_core/_ffmpeg_ops.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def add_video_stream(
9696
_blocks_demuxer_next_packet = (
9797
torch.ops.torchcodec_ns._blocks_demuxer_next_packet.default
9898
)
99+
_blocks_demuxer_seek = torch.ops.torchcodec_ns._blocks_demuxer_seek.default
99100
_blocks_create_packet_decoder = (
100101
torch.ops.torchcodec_ns._blocks_create_packet_decoder.default
101102
)
@@ -105,6 +106,9 @@ def add_video_stream(
105106
_blocks_packet_decoder_send_eof = (
106107
torch.ops.torchcodec_ns._blocks_packet_decoder_send_eof.default
107108
)
109+
_blocks_packet_decoder_reset = (
110+
torch.ops.torchcodec_ns._blocks_packet_decoder_reset.default
111+
)
108112
_blocks_packet_decoder_receive_frame = (
109113
torch.ops.torchcodec_ns._blocks_packet_decoder_receive_frame.default
110114
)

src/torchcodec/_core/custom_ops.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,13 @@ STABLE_TORCH_LIBRARY_FRAGMENT(torchcodec_ns, m) {
7676
m.def(
7777
"_blocks_create_demuxer(str filename, int? stream_index=None) -> Tensor");
7878
m.def("_blocks_demuxer_next_packet(Tensor(a!) demuxer) -> (Tensor, bool)");
79+
m.def("_blocks_demuxer_seek(Tensor(a!) demuxer, float seconds) -> ()");
7980
m.def(
8081
"_blocks_create_packet_decoder(Tensor demuxer, *, int? num_threads=None, str device=\"cpu\") -> Tensor");
8182
m.def(
8283
"_blocks_packet_decoder_send_packet(Tensor(a!) decoder, Tensor packet) -> int");
8384
m.def("_blocks_packet_decoder_send_eof(Tensor(a!) decoder) -> int");
85+
m.def("_blocks_packet_decoder_reset(Tensor(a!) decoder) -> ()");
8486
m.def(
8587
"_blocks_packet_decoder_receive_frame(Tensor(a!) decoder) -> (Tensor, int, float, float, str, Tensor)");
8688
m.def(
@@ -815,6 +817,10 @@ OpsPacketOutput _blocks_demuxer_next_packet(torch::stable::Tensor& demuxer) {
815817
return std::make_tuple(wrap_pointer_to_tensor(std::move(packet)), false);
816818
}
817819

820+
void _blocks_demuxer_seek(torch::stable::Tensor& demuxer, double seconds) {
821+
unwrap_tensor_to_pointer<Demuxer>(demuxer)->seek(seconds);
822+
}
823+
818824
torch::stable::Tensor _blocks_create_packet_decoder(
819825
torch::stable::Tensor& demuxer,
820826
std::optional<int64_t> num_threads,
@@ -844,6 +850,10 @@ int64_t _blocks_packet_decoder_send_eof(torch::stable::Tensor& decoder) {
844850
return static_cast<int64_t>(decoder_ptr->send_eof());
845851
}
846852

853+
void _blocks_packet_decoder_reset(torch::stable::Tensor& decoder) {
854+
unwrap_tensor_to_pointer<PacketDecoder>(decoder)->reset();
855+
}
856+
847857
// TODO_API_BREAKDOWN CC P1: I hate this.
848858
std::string device_to_string(const StableDevice& device) {
849859
std::string name = device_type_name(device.type());
@@ -1498,6 +1508,7 @@ STABLE_TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) {
14981508
m.impl("get_frames_by_pts", TORCH_BOX(&get_frames_by_pts));
14991509
m.impl(
15001510
"_blocks_demuxer_next_packet", TORCH_BOX(&_blocks_demuxer_next_packet));
1511+
m.impl("_blocks_demuxer_seek", TORCH_BOX(&_blocks_demuxer_seek));
15011512
m.impl(
15021513
"_blocks_create_packet_decoder",
15031514
TORCH_BOX(&_blocks_create_packet_decoder));
@@ -1507,6 +1518,8 @@ STABLE_TORCH_LIBRARY_IMPL(torchcodec_ns, CPU, m) {
15071518
m.impl(
15081519
"_blocks_packet_decoder_send_eof",
15091520
TORCH_BOX(&_blocks_packet_decoder_send_eof));
1521+
m.impl(
1522+
"_blocks_packet_decoder_reset", TORCH_BOX(&_blocks_packet_decoder_reset));
15101523
m.impl(
15111524
"_blocks_packet_decoder_receive_frame",
15121525
TORCH_BOX(&_blocks_packet_decoder_receive_frame));

0 commit comments

Comments
 (0)