Skip to content

Commit d830af9

Browse files
scottsNicolasHug
andauthored
Add core support for decoding from Python file-like objects (#564)
Co-authored-by: Nicolas Hug <[email protected]>
1 parent 324b509 commit d830af9

28 files changed

+715
-215
lines changed

.github/workflows/cpp_tests.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ jobs:
3434
python-version: '3.12'
3535
- name: Update pip
3636
run: python -m pip install --upgrade pip
37-
- name: Install dependencies
37+
- name: Install torch dependencies
3838
run: |
3939
python -m pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cpu
40-
- name: Install ffmpeg and pkg-config
40+
- name: Install ffmpeg, pkg-config and pybind11
4141
run: |
42-
conda install "ffmpeg=${{ matrix.ffmpeg-version-for-tests }}" pkg-config -c conda-forge
42+
conda install "ffmpeg=${{ matrix.ffmpeg-version-for-tests }}" pkg-config pybind11 -c conda-forge
4343
ffmpeg -version
4444
- name: Build and run C++ tests
4545
run: |

.github/workflows/docs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
test-infra-repository: pytorch/test-infra
3939
test-infra-ref: main
4040
build-matrix: ${{ needs.generate-matrix.outputs.matrix }}
41+
pre-script: packaging/pre_build_script.sh
4142
post-script: packaging/post_build_script.sh
4243
smoke-test-script: packaging/fake_smoke_test.py
4344
package-name: torchcodec

.github/workflows/lint.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
- name: Install dependencies and FFmpeg
6464
run: |
6565
python -m pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cpu
66-
conda install "ffmpeg=7.0.1" pkg-config -c conda-forge
66+
conda install "ffmpeg=7.0.1" pkg-config pybind11 -c conda-forge
6767
ffmpeg -version
6868
- name: Build and install torchcodec
6969
run: |

.github/workflows/linux_cuda_wheel.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ jobs:
4848
test-infra-repository: pytorch/test-infra
4949
test-infra-ref: main
5050
build-matrix: ${{ needs.generate-matrix.outputs.matrix }}
51+
pre-script: packaging/pre_build_script.sh
5152
post-script: packaging/post_build_script.sh
5253
smoke-test-script: packaging/fake_smoke_test.py
5354
package-name: torchcodec

.github/workflows/linux_wheel.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
test-infra-repository: pytorch/test-infra
5050
test-infra-ref: main
5151
build-matrix: ${{ needs.generate-matrix.outputs.matrix }}
52+
pre-script: packaging/pre_build_script.sh
5253
post-script: packaging/post_build_script.sh
5354
smoke-test-script: packaging/fake_smoke_test.py
5455
package-name: torchcodec

.github/workflows/macos_wheel.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
test-infra-repository: pytorch/test-infra
5050
test-infra-ref: main
5151
build-matrix: ${{ needs.generate-matrix.outputs.matrix }}
52+
pre-script: packaging/pre_build_script.sh
5253
post-script: packaging/post_build_script.sh
5354
smoke-test-script: packaging/fake_smoke_test.py
5455
runner-type: macos-m1-stable

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ test locally you will need the following dependencies:
2020
installation already.
2121
- cmake
2222
- pkg-config
23+
- pybind11
2324
- FFmpeg
2425
- PyTorch nightly
2526

@@ -29,7 +30,7 @@ Start by installing the **nightly** build of PyTorch following the
2930
Then, the easiest way to install the rest of the dependencies is to run:
3031

3132
```bash
32-
conda install cmake pkg-config ffmpeg -c conda-forge
33+
conda install cmake pkg-config pbyind11 ffmpeg -c conda-forge
3334
```
3435

3536
### Clone and build

packaging/pre_build_script.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
set -ex
4+
5+
# We need to install pybind11 because we need its CMake helpers in order to
6+
# compile correctly on Mac. Pybind11 is actually a C++ header-only library,
7+
# and PyTorch actually has it included. PyTorch, however, does not have the
8+
# CMake helpers.
9+
conda install -y pybind11 -c conda-forge

setup.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def run(self):
6868
super().run()
6969

7070
def build_extension(self, ext):
71-
"""Call our CMake build system to build libtorchcodec?.so"""
71+
"""Call our CMake build system to build libtorchcodec*.so"""
7272
# Setuptools was designed to build one extension (.so file) at a time,
7373
# calling this method for each Extension object. We're using a
7474
# CMake-based build where all our extensions are built together at once.
@@ -136,21 +136,27 @@ def copy_extensions_to_source(self):
136136
This is called by setuptools at the end of .run() during editable installs.
137137
"""
138138
self.get_finalized_command("build_py")
139-
extension = ""
139+
extensions = []
140140
if sys.platform == "linux":
141-
extension = "so"
141+
extensions = ["so"]
142142
elif sys.platform == "darwin":
143-
extension = "dylib"
143+
# Mac has BOTH .dylib and .so as library extensions. Short version
144+
# is that a .dylib is a shared library that can be both dynamically
145+
# loaded and depended on by other libraries; a .so can only be a
146+
# dynamically loaded module. For more, see:
147+
# https://stackoverflow.com/a/2339910
148+
extensions = ["dylib", "so"]
144149
else:
145150
raise NotImplementedError(
146151
"Platforms other than linux/darwin are not supported yet"
147152
)
148153

149-
for so_file in self._install_prefix.glob(f"*.{extension}"):
150-
assert "libtorchcodec" in so_file.name
151-
destination = Path("src/torchcodec/") / so_file.name
152-
print(f"Copying {so_file} to {destination}")
153-
self.copy_file(so_file, destination, level=self.verbose)
154+
for ext in extensions:
155+
for lib_file in self._install_prefix.glob(f"*.{ext}"):
156+
assert "libtorchcodec" in lib_file.name
157+
destination = Path("src/torchcodec/") / lib_file.name
158+
print(f"Copying {lib_file} to {destination}")
159+
self.copy_file(lib_file, destination, level=self.verbose)
154160

155161

156162
NOT_A_LICENSE_VIOLATION_VAR = "I_CONFIRM_THIS_IS_NOT_A_LICENSE_VIOLATION"

src/torchcodec/_internally_replaced_utils.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
import importlib
88
import sys
99
from pathlib import Path
10+
from types import ModuleType
1011

1112

1213
# Copy pasted from torchvision
1314
# https://github.com/pytorch/vision/blob/947ae1dc71867f28021d5bc0ff3a19c249236e2a/torchvision/_internally_replaced_utils.py#L25
14-
def _get_extension_path(lib_name):
15+
def _get_extension_path(lib_name: str) -> str:
1516
extension_suffixes = []
1617
if sys.platform == "linux":
1718
extension_suffixes = importlib.machinery.EXTENSION_SUFFIXES
@@ -31,6 +32,22 @@ def _get_extension_path(lib_name):
3132
)
3233
ext_specs = extfinder.find_spec(lib_name)
3334
if ext_specs is None:
34-
raise ImportError
35+
raise ImportError(f"No spec found for {lib_name}")
36+
37+
if ext_specs.origin is None:
38+
raise ImportError(f"Existing spec found for {lib_name} does not have an origin")
3539

3640
return ext_specs.origin
41+
42+
43+
def _load_pybind11_module(module_name: str, library_path: str) -> ModuleType:
44+
spec = importlib.util.spec_from_file_location(
45+
module_name,
46+
library_path,
47+
)
48+
if spec is None:
49+
raise ImportError(
50+
f"Unable to load spec for module {module_name} from path {library_path}"
51+
)
52+
53+
return importlib.util.module_from_spec(spec)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 "src/torchcodec/decoders/_core/AVIOBytesContext.h"
8+
#include <torch/types.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+
int64_t numBytesRead = std::min(
30+
static_cast<int64_t>(buf_size), dataContext->size - dataContext->current);
31+
32+
TORCH_CHECK(
33+
numBytesRead >= 0,
34+
"Tried to read negative bytes: numBytesRead=",
35+
numBytesRead,
36+
", size=",
37+
dataContext->size,
38+
", current=",
39+
dataContext->current);
40+
41+
if (numBytesRead == 0) {
42+
return AVERROR_EOF;
43+
}
44+
45+
std::memcpy(buf, dataContext->data + dataContext->current, numBytesRead);
46+
dataContext->current += numBytesRead;
47+
return numBytesRead;
48+
}
49+
50+
// The signature of this function is defined by FFMPEG.
51+
int64_t AVIOBytesContext::seek(void* opaque, int64_t offset, int whence) {
52+
auto dataContext = static_cast<DataContext*>(opaque);
53+
int64_t ret = -1;
54+
55+
switch (whence) {
56+
case AVSEEK_SIZE:
57+
ret = dataContext->size;
58+
break;
59+
case SEEK_SET:
60+
dataContext->current = offset;
61+
ret = offset;
62+
break;
63+
default:
64+
break;
65+
}
66+
67+
return ret;
68+
}
69+
70+
} // 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+
// Enables users to pass in the entire video as bytes. Our read and seek
14+
// functions then traverse the bytes in memory.
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 "src/torchcodec/decoders/_core/AVIOContextHolder.h"
8+
#include <torch/types.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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
// The AVIOContextHolder serves several purposes:
14+
//
15+
// 1. It is a smart pointer for the AVIOContext. It has the logic to create
16+
// a new AVIOContext and will appropriately free the AVIOContext when it
17+
// goes out of scope. Note that this requires more than just having a
18+
// UniqueAVIOContext, as the AVIOContext points to a buffer which must be
19+
// freed.
20+
// 2. It is a base class for AVIOContext specializations. When specializing a
21+
// AVIOContext, we need to provide four things:
22+
// 1. A read callback function.
23+
// 2. A seek callback function.
24+
// 3. A write callback function. (Not supported yet; it's for encoding.)
25+
// 4. A pointer to some context object that has the same lifetime as the
26+
// AVIOContext itself. This context object holds the custom state that
27+
// tracks the custom behavior of reading, seeking and writing. It is
28+
// provided upon AVIOContext creation and to the read, seek and
29+
// write callback functions.
30+
// While it's not required, it is natural for the derived classes to make
31+
// all of the above members. Base classes need to call
32+
// createAVIOContext(), ideally in their constructor.
33+
// 3. A generic handle for those that just need to manage having access to an
34+
// AVIOContext, but aren't necessarily concerned with how it was customized:
35+
// typically, the VideoDecoder.
36+
class AVIOContextHolder {
37+
public:
38+
virtual ~AVIOContextHolder();
39+
AVIOContext* getAVIOContext();
40+
41+
protected:
42+
// Make constructor protected to prevent anyone from constructing
43+
// an AVIOContextHolder without deriving it. (Ordinarily this would be
44+
// enforced by having a pure virtual methods, but we don't have any.)
45+
AVIOContextHolder() = default;
46+
47+
// These signatures are defined by FFmpeg.
48+
using AVIOReadFunction = int (*)(void*, uint8_t*, int);
49+
using AVIOSeekFunction = int64_t (*)(void*, int64_t, int);
50+
51+
// Deriving classes should call this function in their constructor.
52+
void createAVIOContext(
53+
AVIOReadFunction read,
54+
AVIOSeekFunction seek,
55+
void* heldData,
56+
int bufferSize = defaultBufferSize);
57+
58+
private:
59+
UniqueAVIOContext avioContext_;
60+
61+
// Defaults to 64 KB
62+
static const int defaultBufferSize = 64 * 1024;
63+
};
64+
65+
} // namespace facebook::torchcodec

0 commit comments

Comments
 (0)