Skip to content

Commit 53d0729

Browse files
committed
Forgot the new file. :/
1 parent 1ca8443 commit 53d0729

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// All rights reserved.
3+
//
4+
// This source code is licensed under the BSD-style license found in the
5+
// LICENSE file in the root directory of this source tree.
6+
7+
#include <cstdint>
8+
#include <string>
9+
#include <pybind11/stl.h>
10+
11+
#include "src/torchcodec/decoders/_core/VideoDecoder.h"
12+
13+
namespace py = pybind11;
14+
15+
namespace facebook::torchcodec {
16+
17+
namespace {
18+
19+
// Necessary to make sure that we hold the GIL when we delete a py::object.
20+
struct PyObjectDeleter {
21+
inline void operator()(py::object* obj) const {
22+
if (obj) {
23+
py::gil_scoped_acquire gil;
24+
delete obj;
25+
}
26+
}
27+
};
28+
29+
class AVIOFileLikeContext : public AVIOContextHolder {
30+
public:
31+
AVIOFileLikeContext(py::object fileLike, int bufferSize)
32+
: fileLikeContext_{std::unique_ptr<py::object, PyObjectDeleter>(new py::object(fileLike)), bufferSize} {
33+
{
34+
// TODO: Is it necessary to acquire the GIL here? Is it maybe even harmful? At
35+
// the moment, this is only called from within a pybind function, and pybind
36+
// guarantees we have the GIL.
37+
py::gil_scoped_acquire gil;
38+
TORCH_CHECK(
39+
py::hasattr(fileLike, "read"),
40+
"File like object must implement a read method.");
41+
TORCH_CHECK(
42+
py::hasattr(fileLike, "seek"),
43+
"File like object must implement a seek method.");
44+
}
45+
46+
auto buffer = static_cast<uint8_t*>(av_malloc(bufferSize));
47+
TORCH_CHECK(
48+
buffer != nullptr,
49+
"Failed to allocate buffer of size " + std::to_string(bufferSize));
50+
51+
avioContext_.reset(avio_alloc_context(
52+
buffer,
53+
bufferSize,
54+
0,
55+
&fileLikeContext_,
56+
&AVIOFileLikeContext::read,
57+
nullptr,
58+
&AVIOFileLikeContext::seek));
59+
60+
if (!avioContext_) {
61+
av_freep(&buffer);
62+
TORCH_CHECK(false, "Failed to allocate AVIOContext");
63+
}
64+
}
65+
66+
virtual ~AVIOFileLikeContext() {
67+
if (avioContext_) {
68+
av_freep(&avioContext_->buffer);
69+
}
70+
}
71+
72+
virtual AVIOContext* getAVIOContext() const override {
73+
return avioContext_.get();
74+
}
75+
76+
static int read(void* opaque, uint8_t* buf, int buf_size) {
77+
auto fileLikeContext = static_cast<FileLikeContext*>(opaque);
78+
buf_size = FFMIN(buf_size, fileLikeContext->bufferSize);
79+
80+
int num_read = 0;
81+
while (num_read < buf_size) {
82+
int request = buf_size - num_read;
83+
py::gil_scoped_acquire gil;
84+
auto chunk = static_cast<std::string>(static_cast<py::bytes>(
85+
fileLikeContext->fileLike->attr("read")(request)));
86+
int chunk_len = static_cast<int>(chunk.length());
87+
if (chunk_len == 0) {
88+
break;
89+
}
90+
TORCH_CHECK(
91+
chunk_len <= request,
92+
"Requested up to ",
93+
request,
94+
" bytes but, received ",
95+
chunk_len,
96+
" bytes. The given object does not confirm to read protocol of file object.");
97+
memcpy(buf, chunk.data(), chunk_len);
98+
buf += chunk_len;
99+
num_read += chunk_len;
100+
}
101+
return num_read == 0 ? AVERROR_EOF : num_read;
102+
}
103+
104+
static int64_t seek(void* opaque, int64_t offset, int whence) {
105+
// We do not know the file size.
106+
if (whence == AVSEEK_SIZE) {
107+
return AVERROR(EIO);
108+
}
109+
auto fileLikeContext = static_cast<FileLikeContext*>(opaque);
110+
py::gil_scoped_acquire gil;
111+
return py::cast<int64_t>(
112+
fileLikeContext->fileLike->attr("seek")(offset, whence));
113+
}
114+
115+
private:
116+
struct FileLikeContext {
117+
// Note that we keep a pointer to the Python object because we need to
118+
// strictly control when its destructor is called. We must hold the GIL
119+
// when its destructor gets called, as it needs to update the reference
120+
// count. It's easiest to control that when it's a pointer. Otherwise, we'd
121+
// have to ensure whatever enclosing scope holds the object has the GIL,
122+
// and that's, at least, hard. For all of the common pitfalls, see:
123+
//
124+
// https://pybind11.readthedocs.io/en/stable/advanced/misc.html#common-sources-of-global-interpreter-lock-errors
125+
std::unique_ptr<py::object, PyObjectDeleter> fileLike;
126+
int bufferSize;
127+
};
128+
129+
UniqueAVIOContext avioContext_;
130+
FileLikeContext fileLikeContext_;
131+
};
132+
133+
} // namespace
134+
135+
// In principle, this should be able to return a tensor. But when we try that, we
136+
// run into the bug reported here:
137+
//
138+
// https://github.com/pytorch/pytorch/issues/136664
139+
//
140+
// So we instead launder the pointer through an int, and then use a conversion
141+
// function on the custom ops side to launder that int into a tensor.
142+
int64_t create_from_file_like(
143+
py::object file_like,
144+
std::optional<std::string_view> seek_mode) {
145+
VideoDecoder::SeekMode realSeek = VideoDecoder::SeekMode::exact;
146+
if (seek_mode.has_value()) {
147+
realSeek = seekModeFromString(seek_mode.value());
148+
}
149+
150+
constexpr int bufferSize = 64 * 1024;
151+
auto contextHolder =
152+
std::make_unique<AVIOFileLikeContext>(file_like, bufferSize);
153+
154+
VideoDecoder* decoder = new VideoDecoder(std::move(contextHolder), realSeek);
155+
return reinterpret_cast<int64_t>(decoder);
156+
}
157+
158+
#ifndef TORCHCODEC_PYBIND
159+
#error TORCHCODEC_PYBIND must be defined.
160+
#endif
161+
162+
PYBIND11_MODULE(TORCHCODEC_PYBIND, m) {
163+
m.def("create_from_file_like", &create_from_file_like);
164+
}
165+
166+
} // namespace facebook::torchcodec

0 commit comments

Comments
 (0)