Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Fixes:
- Fix crashes from indexes that were turned into C pointer arithmetic without being range checked. ``MotionVectors[i]`` only checked the upper bound, so a negative index read off the front of the buffer (``mvs[-1]`` now returns the last vector, as with any sequence); ``VideoFormatComponent`` and ``AudioPlane`` accepted any index at all; and ``BitmapSubtitlePlane`` and ``VideoBlockParams`` were missing their lower bounds.
- Frames returned by flushing a codec context directly (``CodecContext.decode()`` with no packet) now carry the stream's ``time_base`` instead of ``None``.
- ``VideoFrame.reformat()`` (and so ``to_ndarray(format=...)``, ``to_rgb()``, ``to_image()``) now shares one ``SwsContext`` per thread instead of allocating one per frame. FFmpeg 8's swscale retains megabytes of graph state per context, which showed up as large RSS growth when many frames were alive at once.
- Building from source against a shared FFmpeg configured with ``--enable-rpath`` no longer fails on ``-Wl,-rpath`` pkg-config flags, and the compiled extensions now embed that rpath.


18.X and Below
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
include *.txt *.md
include setup_cflags.py
recursive-include av *.pyx *.pxd
recursive-include docs *.rst *.py
recursive-include examples *.py
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fate-suite:

lint:
$(PIP) install -U ruff isort pillow numpy mypy==2.1.0 pytest
ruff format --check av examples tests setup.py
ruff format --check av examples tests setup.py setup_cflags.py
isort --check-only --diff av examples tests
mypy av tests

Expand Down
37 changes: 16 additions & 21 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import argparse
import os
import pathlib
import platform
import re
import shlex
import subprocess
import sys

from Cython.Build import cythonize
from Cython.Compiler.AutoDocTransforms import EmbedSignature
from setuptools import Extension, find_packages, setup

from setup_cflags import parse_cflags

FFMPEG_LIBRARIES = [
"avformat",
"avcodec",
Expand Down Expand Up @@ -68,11 +68,14 @@ def get_config_from_directory(ffmpeg_dir):
if not os.path.exists(library_dir):
library_dir = FFMPEG_DIR

return {
config = {
"include_dirs": [include_dir],
"libraries": FFMPEG_LIBRARIES,
"library_dirs": [library_dir],
}
if platform.system() != "Windows":
config["runtime_library_dirs"] = [library_dir]
return config


def get_config_from_pkg_config():
Expand Down Expand Up @@ -102,24 +105,6 @@ def get_config_from_pkg_config():
return known


def parse_cflags(raw_flags):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("-I", dest="include_dirs", action="append")
parser.add_argument("-L", dest="library_dirs", action="append")
parser.add_argument("-l", dest="libraries", action="append")
parser.add_argument("-D", dest="define_macros", action="append")
parser.add_argument("-R", dest="runtime_library_dirs", action="append")

raw_args = shlex.split(raw_flags.strip())
args, unknown = parser.parse_known_args(raw_args)
config = {k: v or [] for k, v in args.__dict__.items()}
for i, x in enumerate(config["define_macros"]):
parts = x.split("=", 1)
value = x[1] or None if len(x) == 2 else None
config["define_macros"][i] = (parts[0], value)
return config, " ".join(shlex.quote(x) for x in unknown)


# Parse command-line arguments.
FFMPEG_DIR = None
for i, arg in enumerate(sys.argv):
Expand All @@ -141,6 +126,14 @@ def parse_cflags(raw_flags):

IMPORT_NAME = "av"

# MSVC cannot embed rpath. An empty list is fine; a path raises
# "don't know how to set runtime library search path for MSVC".
runtime_library_dirs = (
[]
if platform.system() == "Windows"
else extension_extra.get("runtime_library_dirs", [])
)

loudnorm_extension = Extension(
f"{IMPORT_NAME}.filter.loudnorm",
sources=[
Expand All @@ -150,6 +143,7 @@ def parse_cflags(raw_flags):
include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"],
libraries=extension_extra["libraries"],
library_dirs=extension_extra["library_dirs"],
runtime_library_dirs=runtime_library_dirs,
define_macros=define_macros,
py_limited_api=py_limited_api,
)
Expand Down Expand Up @@ -195,6 +189,7 @@ def parse_cflags(raw_flags):
include_dirs=extension_extra["include_dirs"],
libraries=extension_extra["libraries"],
library_dirs=extension_extra["library_dirs"],
runtime_library_dirs=runtime_library_dirs,
sources=[pyx_path],
define_macros=define_macros,
py_limited_api=py_limited_api,
Expand Down
54 changes: 54 additions & 0 deletions setup_cflags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import argparse
import shlex

# GNU ld spellings FFmpeg writes into .pc Libs: when configured with --enable-rpath.
_WL_RPATH_PREFIXES = ("-Wl,-rpath,", "-Wl,-rpath=")


def _rpath_from_wl_flag(flag):
"""Return a path from a GNU rpath linker flag, or None."""
for prefix in _WL_RPATH_PREFIXES:
if flag.startswith(prefix):
path = flag[len(prefix) :]
if path:
return path
return None


def _unique(items):
seen = set()
unique = []
for item in items:
if item not in seen:
seen.add(item)
unique.append(item)
return unique


def parse_cflags(raw_flags):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("-I", dest="include_dirs", action="append")
parser.add_argument("-L", dest="library_dirs", action="append")
parser.add_argument("-l", dest="libraries", action="append")
parser.add_argument("-D", dest="define_macros", action="append")
parser.add_argument("-R", dest="runtime_library_dirs", action="append")

raw_args = shlex.split(raw_flags.strip())
args, unknown = parser.parse_known_args(raw_args)
config = {k: v or [] for k, v in args.__dict__.items()}
for i, x in enumerate(config["define_macros"]):
parts = x.split("=", 1)
value = x[1] or None if len(x) == 2 else None
config["define_macros"][i] = (parts[0], value)

remaining_unknown = []
rpaths = list(config["runtime_library_dirs"])
for flag in unknown:
path = _rpath_from_wl_flag(flag)
if path is not None:
rpaths.append(path)
else:
remaining_unknown.append(flag)

config["runtime_library_dirs"] = _unique(rpaths)
return config, " ".join(shlex.quote(x) for x in remaining_unknown)
73 changes: 73 additions & 0 deletions tests/test_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from setup_cflags import parse_cflags

BASE_FLAGS = "-I/opt/ffmpeg/include -L/opt/ffmpeg/lib -lavcodec"


def test_parse_cflags_known_flags() -> None:
config, unknown = parse_cflags(BASE_FLAGS)

assert unknown == ""
assert config["include_dirs"] == ["/opt/ffmpeg/include"]
assert config["library_dirs"] == ["/opt/ffmpeg/lib"]
assert config["libraries"] == ["avcodec"]
assert config["runtime_library_dirs"] == []


def test_parse_cflags_dash_r() -> None:
config, unknown = parse_cflags(f"{BASE_FLAGS} -R/opt/ffmpeg/lib")

assert unknown == ""
assert config["runtime_library_dirs"] == ["/opt/ffmpeg/lib"]


def test_parse_cflags_wl_rpath_comma() -> None:
config, unknown = parse_cflags(f"{BASE_FLAGS} -Wl,-rpath,/opt/ffmpeg/lib")

assert unknown == ""
assert config["runtime_library_dirs"] == ["/opt/ffmpeg/lib"]


def test_parse_cflags_wl_rpath_equals() -> None:
config, unknown = parse_cflags(f"{BASE_FLAGS} -Wl,-rpath=/opt/ffmpeg/lib")

assert unknown == ""
assert config["runtime_library_dirs"] == ["/opt/ffmpeg/lib"]


def test_parse_cflags_unknown_flag() -> None:
config, unknown = parse_cflags(f"{BASE_FLAGS} -Wl,-z,defs")

assert "-Wl,-z,defs" in unknown
assert config["runtime_library_dirs"] == []


def test_parse_cflags_ffmpeg_enable_rpath_pkg_config() -> None:
raw = (
"-I/opt/ffmpeg/include -L/opt/ffmpeg/lib -lavformat -lavcodec "
"-lavdevice -lavutil -lavfilter -lswscale -lswresample "
"-Wl,-rpath,/opt/ffmpeg/lib"
)
config, unknown = parse_cflags(raw)

assert unknown == ""
assert config["runtime_library_dirs"] == ["/opt/ffmpeg/lib"]
assert config["include_dirs"] == ["/opt/ffmpeg/include"]
assert config["library_dirs"] == ["/opt/ffmpeg/lib"]
assert config["libraries"] == [
"avformat",
"avcodec",
"avdevice",
"avutil",
"avfilter",
"swscale",
"swresample",
]


def test_parse_cflags_deduplicates_rpath() -> None:
config, unknown = parse_cflags(
f"{BASE_FLAGS} -R/opt/ffmpeg/lib -Wl,-rpath,/opt/ffmpeg/lib"
)

assert unknown == ""
assert config["runtime_library_dirs"] == ["/opt/ffmpeg/lib"]
Loading