Skip to content

Commit 5bb47ab

Browse files
author
pytorchbot
committed
2026-07-24 nightly release (93195e5)
1 parent 4316c00 commit 5bb47ab

33 files changed

Lines changed: 1822 additions & 801 deletions

.github/workflows/linux_cuda_wheel.yaml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,65 @@ jobs:
196196
run: |
197197
pytest --override-ini="addopts=-v" test --tb=short
198198
199+
install-and-test-no-ffmpeg:
200+
# Asserts that the CUDA wheel works WITHOUT FFmpeg installed: it must import,
201+
# the FFmpeg-free image decoders must work, and the FFmpeg-backed entry
202+
# points must fail with a clear error.
203+
runs-on: ubuntu-latest
204+
needs: build
205+
env:
206+
PYTHON_VERSION: '3.10'
207+
CUDA_VERSION: '12.6'
208+
steps:
209+
- name: Setup env vars
210+
run: |
211+
cuda_version_without_periods=$(echo "${{ env.CUDA_VERSION }}" | sed 's/\.//g')
212+
echo cuda_version_without_periods=${cuda_version_without_periods} >> $GITHUB_ENV
213+
python_version_without_periods=$(echo "${{ env.PYTHON_VERSION }}" | sed 's/\.//g')
214+
echo python_version_without_periods=${python_version_without_periods} >> $GITHUB_ENV
215+
216+
- name: Check out repo
217+
uses: actions/checkout@v6
218+
219+
- name: Remove src/ folder
220+
run: bash packaging/remove_src.sh
221+
222+
- name: Setup conda env
223+
uses: conda-incubator/setup-miniconda@v3
224+
with:
225+
auto-update-conda: true
226+
miniforge-version: latest
227+
activate-environment: test
228+
python-version: ${{ env.PYTHON_VERSION }}
229+
230+
- name: Update pip
231+
run: python -m pip install --upgrade pip
232+
233+
- name: Install PyTorch
234+
run: bash packaging/install_pytorch.sh cu${{ env.cuda_version_without_periods }} "torch torchvision"
235+
236+
- uses: actions/download-artifact@v4
237+
with:
238+
name: meta-pytorch_torchcodec__${{ env.PYTHON_VERSION }}_cu${{ env.cuda_version_without_periods }}_x86_64
239+
path: dist/
240+
241+
- name: Install torchcodec from the wheel
242+
run: bash packaging/install_torchcodec_wheel.sh "*cu${{ env.cuda_version_without_periods }}-cp${{ env.python_version_without_periods }}*.whl"
243+
244+
- name: Install test dependencies
245+
run: bash packaging/install_test_dependencies.sh
246+
247+
- name: Assert FFmpeg is absent and torchcodec still imports
248+
run: |
249+
source packaging/helpers.sh
250+
assert_ffmpeg_not_installed
251+
python -c "import torchcodec; from torchcodec._core import ops; assert not ops._FFMPEG_AVAILABLE, 'FFmpeg was unexpectedly loaded'; print('OK: torchcodec imported without FFmpeg')"
252+
253+
- name: Run FFmpeg-free Python tests
254+
run: |
255+
FAIL_WITHOUT_JPEG=1 FAIL_WITHOUT_PNG=1 FAIL_WITHOUT_WEBP=1 FAIL_WITHOUT_AVIF=1 \
256+
pytest --override-ini="addopts=-v" test/test_ffmpeg_optional.py test/test_decoders.py::TestImageDecoder
257+
199258
build-docs:
200259
runs-on: linux.g5.4xlarge.nvidia.gpu
201260
env:

.github/workflows/linux_wheel.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,60 @@ jobs:
109109
run: |
110110
FAIL_WITHOUT_JPEG=1 FAIL_WITHOUT_PNG=1 FAIL_WITHOUT_WEBP=1 FAIL_WITHOUT_AVIF=1 pytest --override-ini="addopts=-v" test
111111
112+
install-and-test-no-ffmpeg:
113+
# Asserts that torchcodec works WITHOUT FFmpeg installed: the package must
114+
# import, the FFmpeg-free image decoders must work, and the FFmpeg-backed
115+
# entry points must fail with a clear error.
116+
runs-on: ubuntu-latest
117+
strategy:
118+
fail-fast: false
119+
matrix:
120+
python-version: ['3.10']
121+
needs: build
122+
steps:
123+
- name: Check out repo
124+
uses: actions/checkout@v6
125+
126+
- name: Remove src/ folder
127+
run: bash packaging/remove_src.sh
128+
129+
- name: Setup conda env
130+
uses: conda-incubator/setup-miniconda@v3
131+
with:
132+
auto-update-conda: true
133+
miniforge-version: latest
134+
activate-environment: test
135+
python-version: ${{ matrix.python-version }}
136+
137+
- name: Update pip
138+
run: python -m pip install --upgrade pip
139+
140+
- name: Install PyTorch
141+
run: bash packaging/install_pytorch.sh cpu "torch torchvision"
142+
143+
- uses: actions/download-artifact@v4
144+
with:
145+
name: meta-pytorch_torchcodec__${{ matrix.python-version }}_cpu_x86_64
146+
path: dist/
147+
148+
- name: Install torchcodec from the wheel
149+
run: bash packaging/install_torchcodec_wheel.sh
150+
151+
- name: Install test dependencies
152+
run: bash packaging/install_test_dependencies.sh
153+
154+
# Deliberately NO "Install ffmpeg" step here.
155+
- name: Assert FFmpeg is absent and torchcodec still imports
156+
run: |
157+
source packaging/helpers.sh
158+
assert_ffmpeg_not_installed
159+
python -c "import torchcodec; from torchcodec._core import ops; assert not ops._FFMPEG_AVAILABLE, 'FFmpeg was unexpectedly loaded'; print('OK: torchcodec imported without FFmpeg')"
160+
161+
- name: Run FFmpeg-free Python tests
162+
run: |
163+
FAIL_WITHOUT_JPEG=1 FAIL_WITHOUT_PNG=1 FAIL_WITHOUT_WEBP=1 FAIL_WITHOUT_AVIF=1 \
164+
pytest --override-ini="addopts=-v" test/test_ffmpeg_optional.py test/test_decoders.py::TestImageDecoder
165+
112166
install-and-test-third-party-interface:
113167
runs-on: ubuntu-latest
114168
strategy:

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ videos and audio, or run inference, TorchCodec is how you turn these into data.
1111
We achieve these capabilities through:
1212

1313
* Pythonic APIs that mirror Python and PyTorch conventions.
14-
* Relying on [FFmpeg](https://www.ffmpeg.org/) to do the decoding and encoding.
15-
TorchCodec uses the version of FFmpeg you already have installed. FFmpeg is a
16-
mature library with broad coverage available on most systems. It is, however,
17-
not easy to use. TorchCodec abstracts FFmpeg's complexity to ensure it is used
18-
correctly and efficiently.
14+
* Relying on [FFmpeg](https://www.ffmpeg.org/) to do the video and audio
15+
decoding and encoding. TorchCodec uses the version of FFmpeg you already have
16+
installed. FFmpeg is a mature library with broad coverage available on most
17+
systems. It is, however, not easy to use. TorchCodec abstracts FFmpeg's
18+
complexity to ensure it is used correctly and efficiently. (FFmpeg is
19+
optional, and the image decoders don't need it: see [Installing
20+
TorchCodec](#installing-torchcodec).)
1921
* Returning data as PyTorch tensors, ready to be fed into PyTorch transforms
2022
or used directly to train models.
2123

@@ -105,6 +107,13 @@ with encoder.open_file("output.mp4"):
105107
conda install "ffmpeg" -c conda-forge
106108
```
107109

110+
> **Note:** FFmpeg is an *optional* dependency. It is needed for video
111+
> and audio decoding and encoding (`VideoDecoder`, `AudioDecoder`,
112+
> `VideoEncoder`, `AudioEncoder`, etc.). The image decoders
113+
> (`decode_image`, `decode_jpeg`, `decode_png`, etc.)
114+
> do **not** require FFmpeg, so if you only need image decoding you can skip
115+
> this step.
116+
108117
2. Install PyTorch and TorchCodec:
109118

110119
```bash

src/torchcodec/_core/DecodeAvif.cpp

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
namespace facebook::torchcodec {
1818

1919
torch::stable::Tensor decode_avif(
20-
[[maybe_unused]] const torch::stable::Tensor& data,
21-
[[maybe_unused]] int64_t mode) {
20+
[[maybe_unused]] const torch::stable::Tensor& input,
21+
[[maybe_unused]] int64_t mode,
22+
[[maybe_unused]] int64_t output_dtype) {
2223
STD_TORCH_CHECK(
2324
false,
2425
"decode_avif: torchcodec was not compiled with libavif support. "
@@ -88,14 +89,20 @@ ExifOrientation avif_exif_orientation(const avifImage* image) {
8889

8990
torch::stable::Tensor decode_avif(
9091
const torch::stable::Tensor& input,
91-
int64_t mode) {
92+
int64_t mode,
93+
int64_t output_dtype,
94+
int64_t num_threads) {
9295
// Based on
9396
// https://github.com/AOMediaCodec/libavif/blob/main/examples/avif_example_decode_memory.c
9497
validate_encoded_data(input);
98+
STD_TORCH_CHECK(
99+
num_threads >= 1, "num_threads must be >= 1, got ", num_threads);
95100

96101
DecoderPtr decoder(avifDecoderCreate());
97102
STD_TORCH_CHECK(decoder != nullptr, "Failed to create avif decoder.");
98103

104+
decoder->maxThreads = static_cast<int>(num_threads);
105+
99106
auto result = avifDecoderSetIOMemory(
100107
decoder.get(), input.const_data_ptr<uint8_t>(), input.numel());
101108
STD_TORCH_CHECK(
@@ -124,6 +131,9 @@ torch::stable::Tensor decode_avif(
124131
static_cast<bool>(decoder->alphaPresent));
125132
int num_channels = return_rgb ? 3 : 4;
126133

134+
bool output_16 = should_output_uint16(
135+
static_cast<OutputDtype>(output_dtype), decoder->image->depth > 8);
136+
127137
torch::stable::Tensor output;
128138
uint8_t* output_ptr = nullptr;
129139
int64_t frame_num_bytes = 0;
@@ -141,8 +151,7 @@ torch::stable::Tensor decode_avif(
141151
std::memset(&rgb, 0, sizeof(rgb));
142152
avifRGBImageSetDefaults(&rgb, decoder->image);
143153

144-
// TODO_IMAGE: support 10 and 12 bits.
145-
rgb.depth = 8;
154+
rgb.depth = output_16 ? 16 : 8;
146155
rgb.format = return_rgb ? AVIF_RGB_FORMAT_RGB : AVIF_RGB_FORMAT_RGBA;
147156
rgb.ignoreAlpha = return_rgb ? AVIF_TRUE : AVIF_FALSE;
148157

@@ -152,14 +161,14 @@ torch::stable::Tensor decode_avif(
152161
num_channels,
153162
static_cast<int64_t>(rgb.height),
154163
static_cast<int64_t>(rgb.width)},
155-
kStableUInt8,
164+
output_16 ? kStableUInt16 : kStableUInt8,
156165
std::nullopt,
157166
std::nullopt,
158167
std::nullopt,
159168
torch::headeronly::MemoryFormat::ChannelsLast);
160-
output_ptr = output.mutable_data_ptr<uint8_t>();
161-
frame_num_bytes =
162-
static_cast<int64_t>(num_channels) * rgb.height * rgb.width;
169+
output_ptr = static_cast<uint8_t*>(output.mutable_data_ptr());
170+
frame_num_bytes = static_cast<int64_t>(num_channels) * rgb.height *
171+
rgb.width * (output_16 ? 2 : 1);
163172
}
164173

165174
rgb.pixels = output_ptr + i * frame_num_bytes;

src/torchcodec/_core/DecodeAvif.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
namespace facebook::torchcodec {
1212

1313
FORCE_PUBLIC_VISIBILITY torch::stable::Tensor decode_avif(
14-
const torch::stable::Tensor& data,
15-
int64_t mode);
14+
const torch::stable::Tensor& input,
15+
int64_t mode,
16+
int64_t output_dtype,
17+
int64_t num_threads);
1618

1719
} // namespace facebook::torchcodec

src/torchcodec/_core/DecodeGif.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace facebook::torchcodec {
1717
// RGB_ALPHA produces a 4-channel RGBA tensor preserving transparency as alpha;
1818
// UNCHANGED produces RGBA if the GIF has any transparency, else RGB.
1919
FORCE_PUBLIC_VISIBILITY torch::stable::Tensor decode_gif(
20-
const torch::stable::Tensor& data,
20+
const torch::stable::Tensor& input,
2121
int64_t mode);
2222

2323
} // namespace facebook::torchcodec

src/torchcodec/_core/DecodeJpeg.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
namespace facebook::torchcodec {
1818

1919
torch::stable::Tensor decode_jpeg(
20-
[[maybe_unused]] const torch::stable::Tensor& data,
20+
[[maybe_unused]] const torch::stable::Tensor& input,
2121
[[maybe_unused]] int64_t mode) {
2222
STD_TORCH_CHECK(
2323
false,
@@ -300,6 +300,8 @@ void decode_rows(
300300
}
301301
output_ptr += stride;
302302
}
303+
304+
jpeg_finish_decompress(&jpeg_ctx);
303305
}
304306

305307
} // namespace
@@ -417,9 +419,6 @@ void decode_rows(
417419
//
418420
/* clang-format on */
419421

420-
// TODO_IMAGE: align names. everywhere. this is input, other places it's data,
421-
// other places it's something else. Should align here, in the header, in the
422-
// custom op definition, etc. Across codecs.
423422
torch::stable::Tensor decode_jpeg(
424423
const torch::stable::Tensor& input,
425424
int64_t mode) {
@@ -440,6 +439,10 @@ torch::stable::Tensor decode_jpeg(
440439
input.numel(),
441440
static_cast<ImageReadMode>(mode));
442441

442+
// Note: this must be called before jpeg_finish_decompress(), otherwise we get
443+
// garbage exif data.
444+
ExifOrientation exif_orientation = fetch_jpeg_exif_orientation(&jpeg_ctx);
445+
443446
// We want output to be channels last
444447
int64_t stride =
445448
static_cast<int64_t>(jpeg_ctx.output_width) * num_output_channels;
@@ -469,11 +472,16 @@ torch::stable::Tensor decode_jpeg(
469472

470473
decode_rows(jpeg_ctx, error_ctx, output_ptr, stride, cmyk_helper);
471474

472-
// EXIF markers were parsed during jpeg_read_header so this is just an
473-
// in-memory lookup (i.e. we're not going back to the beginning of the file)
474-
ExifOrientation exif_orientation = fetch_jpeg_exif_orientation(&jpeg_ctx);
475+
// Flip raw CMYK samples so our output matches PIL.
476+
if (static_cast<ImageReadMode>(mode) == ImageReadMode::UNCHANGED &&
477+
(jpeg_ctx.jpeg_color_space == JCS_CMYK ||
478+
jpeg_ctx.jpeg_color_space == JCS_YCCK)) {
479+
int64_t num_bytes = output.numel();
480+
for (int64_t i = 0; i < num_bytes; ++i) {
481+
output_ptr[i] = static_cast<uint8_t>(255 - output_ptr[i]);
482+
}
483+
}
475484

476-
jpeg_finish_decompress(&jpeg_ctx);
477485
jpeg_destroy_decompress(&jpeg_ctx);
478486
return exif_orientation_transform(
479487
stable_permute(output, {2, 0, 1}), exif_orientation);

src/torchcodec/_core/DecodeJpeg.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
namespace facebook::torchcodec {
1212

1313
FORCE_PUBLIC_VISIBILITY torch::stable::Tensor decode_jpeg(
14-
const torch::stable::Tensor& data,
14+
const torch::stable::Tensor& input,
1515
int64_t mode);
1616

1717
} // namespace facebook::torchcodec

0 commit comments

Comments
 (0)