Skip to content

Commit a76d6a0

Browse files
committed
Break out AVIOContext stuff into their own header and source files
1 parent d301f53 commit a76d6a0

14 files changed

+363
-248
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 <torch/types.h>
8+
#include "src/torchcodec/decoders/_core/AVIOBytesContext.h"
9+
10+
namespace facebook::torchcodec {
11+
12+
AVIOBytesContext::AVIOBytesContext(const void* data, int64_t dataSize)
13+
: dataContext_{static_cast<const uint8_t*>(data), dataSize, 0} {
14+
TORCH_CHECK(data != nullptr, "Video data buffer cannot be nullptr!");
15+
TORCH_CHECK(dataSize > 0, "Video data size must be positive");
16+
createAVIOContext(&read, &seek, &dataContext_);
17+
}
18+
19+
// The signature of this function is defined by FFMPEG.
20+
int AVIOBytesContext::read(void* opaque, uint8_t* buf, int buf_size) {
21+
auto dataContext = static_cast<DataContext*>(opaque);
22+
TORCH_CHECK(
23+
dataContext->current <= dataContext->size,
24+
"Tried to read outside of the buffer: current=",
25+
dataContext->current,
26+
", size=",
27+
dataContext->size);
28+
29+
buf_size = FFMIN(
30+
buf_size, static_cast<int>(dataContext->size - dataContext->current));
31+
TORCH_CHECK(
32+
buf_size >= 0,
33+
"Tried to read negative bytes: buf_size=",
34+
buf_size,
35+
", size=",
36+
dataContext->size,
37+
", current=",
38+
dataContext->current);
39+
40+
if (!buf_size) {
41+
return AVERROR_EOF;
42+
}
43+
memcpy(buf, dataContext->data + dataContext->current, buf_size);
44+
dataContext->current += buf_size;
45+
return buf_size;
46+
}
47+
48+
// The signature of this function is defined by FFMPEG.
49+
int64_t AVIOBytesContext::seek(void* opaque, int64_t offset, int whence) {
50+
auto dataContext = static_cast<DataContext*>(opaque);
51+
int64_t ret = -1;
52+
53+
switch (whence) {
54+
case AVSEEK_SIZE:
55+
ret = dataContext->size;
56+
break;
57+
case SEEK_SET:
58+
dataContext->current = offset;
59+
ret = offset;
60+
break;
61+
default:
62+
break;
63+
}
64+
65+
return ret;
66+
}
67+
68+
} // namespace facebook::torchcodec
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
#pragma once
8+
9+
#include "src/torchcodec/decoders/_core/AVIOContextHolder.h"
10+
11+
namespace facebook::torchcodec {
12+
13+
// TODO: make comment below better
14+
// memory buffer that is passed in.
15+
class AVIOBytesContext : public AVIOContextHolder {
16+
public:
17+
explicit AVIOBytesContext(const void* data, int64_t dataSize);
18+
19+
private:
20+
struct DataContext {
21+
const uint8_t* data;
22+
int64_t size;
23+
int64_t current;
24+
};
25+
26+
static int read(void* opaque, uint8_t* buf, int buf_size);
27+
static int64_t seek(void* opaque, int64_t offset, int whence);
28+
29+
DataContext dataContext_;
30+
};
31+
32+
} // namespace facebook::torchcodec
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 <torch/types.h>
8+
#include "src/torchcodec/decoders/_core/AVIOContextHolder.h"
9+
10+
namespace facebook::torchcodec {
11+
12+
void AVIOContextHolder::createAVIOContext(
13+
AVIOReadFunction read,
14+
AVIOSeekFunction seek,
15+
void* heldData,
16+
int bufferSize) {
17+
TORCH_CHECK(
18+
bufferSize > 0,
19+
"Buffer size must be greater than 0; is " + std::to_string(bufferSize));
20+
auto buffer = static_cast<uint8_t*>(av_malloc(bufferSize));
21+
TORCH_CHECK(
22+
buffer != nullptr,
23+
"Failed to allocate buffer of size " + std::to_string(bufferSize));
24+
25+
avioContext_.reset(avio_alloc_context(
26+
buffer,
27+
bufferSize,
28+
0,
29+
heldData,
30+
read,
31+
nullptr, // write function; not supported yet
32+
seek));
33+
34+
if (!avioContext_) {
35+
av_freep(&buffer);
36+
TORCH_CHECK(false, "Failed to allocate AVIOContext");
37+
}
38+
}
39+
40+
AVIOContextHolder::~AVIOContextHolder() {
41+
if (avioContext_) {
42+
av_freep(&avioContext_->buffer);
43+
}
44+
}
45+
46+
AVIOContext* AVIOContextHolder::getAVIOContext() {
47+
return avioContext_.get();
48+
}
49+
50+
} // namespace facebook::torchcodec
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
#pragma once
8+
9+
#include "src/torchcodec/decoders/_core/FFMPEGCommon.h"
10+
11+
namespace facebook::torchcodec {
12+
13+
// These signatures are defined by FFmpeg.
14+
using AVIOReadFunction = int (*)(void*, uint8_t*, int);
15+
using AVIOSeekFunction = int64_t (*)(void*, int64_t, int);
16+
17+
// The AVIOContextHolder serves several purposes:
18+
//
19+
// 1. It is a smart pointer for the AVIOContext. It has the logic to create
20+
// a new AVIOContext and will appropriately free the AVIOContext when it
21+
// goes out of scope. Note that this requires more than just the having a
22+
// UniqueAVIOContext, as the AVIOContext points to a buffer which must be
23+
// freed.
24+
// 2. It is a base class for AVIOContext specializations. When specializing a
25+
// AVIOContext, we need to provide four things:
26+
// 1. A read callback function.
27+
// 2. A seek callback function.
28+
// 3. A write callback function. (Not supported yet; it's for encoding.)
29+
// 4. A pointer to some context object that has the same lifetime as the
30+
// AVIOContext itself. This context object holds the custom state that
31+
// tracks the custom behavior of reading, seeking and writing. It is
32+
// provided upon AVIOContext creation and to the read, seek and
33+
// write callback functions.
34+
// While it's not required, it is natural for the derived classes to make
35+
// all of the above members. Base classes need to call
36+
// createAVIOContext(), ideally in there constructor.
37+
// 3. A generic handle for those that just need to manage having access to an
38+
// AVIOContext, but aren't necessarily concerned with how it was customized.
39+
class AVIOContextHolder {
40+
public:
41+
virtual ~AVIOContextHolder();
42+
AVIOContext* getAVIOContext();
43+
44+
protected:
45+
// Make constructor protected to prevent anyone from constructing
46+
// an AVIOContextHolder without deriving it. (Ordinarily this would be
47+
// enforced by having a pure virtual methods, but we don't have any.)
48+
AVIOContextHolder() = default;
49+
50+
// Deriving classes should call this function in their constructor.
51+
void createAVIOContext(
52+
AVIOReadFunction read,
53+
AVIOSeekFunction seek,
54+
void* heldData,
55+
int bufferSize = defaultBufferSize);
56+
57+
private:
58+
UniqueAVIOContext avioContext_;
59+
60+
// Defaults to 64 KB
61+
static const int defaultBufferSize = 64 * 1024;
62+
};
63+
64+
} // namespace facebook::torchcodec
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 <torch/types.h>
8+
#include "src/torchcodec/decoders/_core/AVIOFileLikeContext.h"
9+
10+
namespace facebook::torchcodec {
11+
12+
AVIOFileLikeContext::AVIOFileLikeContext(py::object fileLike)
13+
: fileLike_{UniquePyObject(new py::object(fileLike))} {
14+
{
15+
// TODO: Is it necessary to acquire the GIL here? Is it maybe even
16+
// harmful? At the moment, this is only called from within a pybind
17+
// function, and pybind guarantees we have the GIL.
18+
py::gil_scoped_acquire gil;
19+
TORCH_CHECK(
20+
py::hasattr(fileLike, "read"),
21+
"File like object must implement a read method.");
22+
TORCH_CHECK(
23+
py::hasattr(fileLike, "seek"),
24+
"File like object must implement a seek method.");
25+
}
26+
createAVIOContext(&read, &seek, &fileLike_);
27+
}
28+
29+
int AVIOFileLikeContext::read(void* opaque, uint8_t* buf, int buf_size) {
30+
auto fileLike = static_cast<UniquePyObject*>(opaque);
31+
32+
int num_read = 0;
33+
while (num_read < buf_size) {
34+
int request = buf_size - num_read;
35+
// TODO: It is maybe more efficient to grab the lock once in the
36+
// surrounding scope?
37+
py::gil_scoped_acquire gil;
38+
auto chunk = static_cast<std::string>(
39+
static_cast<py::bytes>((*fileLike)->attr("read")(request)));
40+
int chunk_len = static_cast<int>(chunk.length());
41+
if (chunk_len == 0) {
42+
break;
43+
}
44+
TORCH_CHECK(
45+
chunk_len <= request,
46+
"Requested up to ",
47+
request,
48+
" bytes but, received ",
49+
chunk_len,
50+
" bytes. The given object does not confirm to read protocol of file object.");
51+
memcpy(buf, chunk.data(), chunk_len);
52+
buf += chunk_len;
53+
num_read += chunk_len;
54+
}
55+
return num_read == 0 ? AVERROR_EOF : num_read;
56+
}
57+
58+
int64_t AVIOFileLikeContext::seek(void* opaque, int64_t offset, int whence) {
59+
// We do not know the file size.
60+
if (whence == AVSEEK_SIZE) {
61+
return AVERROR(EIO);
62+
}
63+
auto fileLike = static_cast<UniquePyObject*>(opaque);
64+
py::gil_scoped_acquire gil;
65+
return py::cast<int64_t>((*fileLike)->attr("seek")(offset, whence));
66+
}
67+
68+
} // namespace facebook::torchcodec
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
#pragma once
8+
9+
#include <pybind11/pybind11.h>
10+
#include <pybind11/stl.h>
11+
12+
#include "src/torchcodec/decoders/_core/AVIOContextHolder.h"
13+
14+
namespace py = pybind11;
15+
16+
namespace facebook::torchcodec {
17+
18+
// Necessary to make sure that we hold the GIL when we delete a py::object.
19+
struct PyObjectDeleter {
20+
inline void operator()(py::object* obj) const {
21+
if (obj) {
22+
py::gil_scoped_acquire gil;
23+
delete obj;
24+
}
25+
}
26+
};
27+
28+
using UniquePyObject = std::unique_ptr<py::object, PyObjectDeleter>;
29+
30+
class AVIOFileLikeContext : public AVIOContextHolder {
31+
public:
32+
explicit AVIOFileLikeContext(py::object fileLike);
33+
34+
private:
35+
static int read(void* opaque, uint8_t* buf, int buf_size);
36+
static int64_t seek(void* opaque, int64_t offset, int whence);
37+
38+
// Note that we dynamically allocate the Python object because we need to
39+
// strictly control when its destructor is called. We must hold the GIL
40+
// when its destructor gets called, as it needs to update the reference
41+
// count. It's easiest to control that when it's dynamic memory. Otherwise,
42+
// we'd have to ensure whatever enclosing scope holds the object has the GIL,
43+
// and that's, at least, hard. For all of the common pitfalls, see:
44+
//
45+
// https://pybind11.readthedocs.io/en/stable/advanced/misc.html#common-sources-of-global-interpreter-lock-errors
46+
UniquePyObject fileLike_;
47+
};
48+
49+
} // namespace facebook::torchcodec

src/torchcodec/decoders/_core/CMakeLists.txt

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ function(make_torchcodec_libraries
4040

4141
# Create libtorchcodec_decoderN.so
4242
set(decoder_library_name "libtorchcodec_decoder${ffmpeg_major_version}")
43-
set(decoder_sources FFMPEGCommon.cpp VideoDecoder.cpp)
43+
set(decoder_sources
44+
AVIOContextHolder.cpp
45+
FFMPEGCommon.cpp
46+
VideoDecoder.cpp
47+
)
4448

4549
if(ENABLE_CUDA)
4650
list(APPEND decoder_sources CudaDevice.cpp)
@@ -70,7 +74,10 @@ function(make_torchcodec_libraries
7074

7175
# Create libtorchcodec_custom_opsN.so
7276
set(custom_ops_library_name "libtorchcodec_custom_ops${ffmpeg_major_version}")
73-
set(custom_ops_sources VideoDecoderOps.cpp)
77+
set(custom_ops_sources
78+
AVIOBytesContext.cpp
79+
VideoDecoderOps.cpp
80+
)
7481
make_torchcodec_sublibrary(
7582
"${custom_ops_library_name}"
7683
"${custom_ops_sources}"
@@ -80,7 +87,10 @@ function(make_torchcodec_libraries
8087

8188
# Create libtorchcodec_pybind_opsN.so
8289
set(pybind_ops_library_name "libtorchcodec_pybind_ops${ffmpeg_major_version}")
83-
set(pybind_ops_sources PyBindOps.cpp)
90+
set(pybind_ops_sources
91+
AVIOFileLikeContext.cpp
92+
PyBindOps.cpp
93+
)
8494
make_torchcodec_sublibrary(
8595
"${pybind_ops_library_name}"
8696
"${pybind_ops_sources}"
@@ -92,6 +102,13 @@ function(make_torchcodec_libraries
92102
PUBLIC
93103
TORCHCODEC_PYBIND=_torchcodec_pybind_ops${ffmpeg_major_version}
94104
)
105+
# pybind11 quirk, see:
106+
# https://pybind11.readthedocs.io/en/stable/faq.html#someclass-declared-with-greater-visibility-than-the-type-of-its-field-someclass-member-wattributes
107+
target_compile_options(
108+
${pybind_ops_library_name}
109+
PUBLIC
110+
"-fvisibility=hidden"
111+
)
95112

96113
# Install all libraries.
97114
set(
@@ -172,7 +189,8 @@ else()
172189

173190
# Expose these values updwards so that the test compilation does not need
174191
# to re-figure it out. FIXME: it's not great that we just copy-paste the
175-
# library name.
192+
# library names.
176193
set(libtorchcodec_library_name "libtorchcodec_decoder${ffmpeg_major_version}" PARENT_SCOPE)
194+
set(libtorchcodec_custom_ops_name "libtorchcodec_custom_ops${ffmpeg_major_version}" PARENT_SCOPE)
177195
set(libav_include_dirs ${LIBAV_INCLUDE_DIRS} PARENT_SCOPE)
178196
endif()

0 commit comments

Comments
 (0)