Files
plezy/linux/packaging/build-libmpv.sh
T
edde746 ae001d9ff3 fix(linux): restore VAAPI hardware decode and AV1 software fallback in the bundled libmpv
Hardware decoding stopped working for Linux users on 2.13.0 (Fedora 44
report): every source decodes in software, and AV1 plays black video with
audio. Two defects in the pinned libmpv build.

First, mpv's meson 'drm' feature silently disabled itself because the CI
builder lacks libdisplay-info, and every VAAPI path that does not depend
on a display server is derived from it: vaapi-copy's standalone render-node
device (the path 2.12.1 worked on) and the GL dmabuf interop for direct
vaapi. With only the Wayland VA provider compiled in, a machine whose
Wayland VA display fails to initialize has no fallback, and vaapi-copy
has an empty provider list - every source lands on software decoding.
Pin -Ddrm=enabled, -Dvaapi-drm=enabled, -Degl=enabled and
-Dvaapi-wayland=enabled, and add libdisplay-info-dev to the CI package
lists, so a missing piece fails the build instead of shipping silent
software decode.

Second, the bundled static FFmpeg has no AV1 software decoder: its native
av1 codec is hardware-accelerated only, so once hwdec fails there is no AV1
path at all - every packet errors, video hits EOF, the plane goes black
while audio keeps playing. Pin dav1d 1.5.4 (both VideoLAN remotes agree on
the tag object and root commit), build it static before ffmpeg, and pass
--enable-libdav1d.

The build-plan stub test now asserts the hwdec feature flags, the dav1d
static build, and ffmpeg's libdav1d. Verified in an ubuntu:24.04 container
with the production flag sets: meson reports drm, vaapi-drm, vaapi-wayland,
egl and dmabuf-interop-gl enabled, and ffmpeg configures CONFIG_LIBDAV1D=yes
with the AV1 VAAPI hwaccel.

close #1874
2026-08-14 21:49:41 +02:00

359 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
NATIVE_INPUTS_MANIFEST="${NATIVE_INPUTS_MANIFEST:-$SCRIPT_DIR/native-inputs.json}"
manifest_value() {
python3 - "$NATIVE_INPUTS_MANIFEST" "$1" "$2" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as source:
manifest = json.load(source)
value = manifest["inputs"][sys.argv[2]][sys.argv[3]]
if not isinstance(value, str) or not value:
raise SystemExit(f"invalid manifest value: {sys.argv[2]}.{sys.argv[3]}")
print(value)
PY
}
# For keys that are genuinely optional, where absence means "not offered"
# rather than a broken manifest. Pinned values never come through here: a
# missing checksum or commit has to stay fatal.
manifest_optional() {
python3 - "$NATIVE_INPUTS_MANIFEST" "$1" "$2" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as source:
manifest = json.load(source)
value = manifest["inputs"][sys.argv[2]].get(sys.argv[3], "")
if not isinstance(value, str):
raise SystemExit(f"invalid manifest value: {sys.argv[2]}.{sys.argv[3]}")
print(value)
PY
}
FFMPEG_VERSION="$(manifest_value ffmpeg version)"
FFMPEG_URL="$(manifest_value ffmpeg url)"
FFMPEG_SHA256="$(manifest_value ffmpeg sha256)"
DAV1D_VERSION="$(manifest_value dav1d version)"
DAV1D_URL="$(manifest_value dav1d url)"
DAV1D_MIRROR="$(manifest_optional dav1d mirror)"
DAV1D_REF="$(manifest_value dav1d ref)"
DAV1D_COMMIT="$(manifest_value dav1d commit)"
SHADERC_VERSION="$(manifest_value shaderc version)"
SHADERC_URL="$(manifest_value shaderc url)"
SHADERC_REF="$(manifest_value shaderc ref)"
SHADERC_COMMIT="$(manifest_value shaderc commit)"
LIBPLACEBO_VERSION="$(manifest_value libplacebo version)"
LIBPLACEBO_URL="$(manifest_value libplacebo url)"
LIBPLACEBO_MIRROR="$(manifest_optional libplacebo mirror)"
LIBPLACEBO_REF="$(manifest_value libplacebo ref)"
LIBPLACEBO_COMMIT="$(manifest_value libplacebo commit)"
MPV_VERSION="$(manifest_value mpv version)"
MPV_URL="$(manifest_value mpv url)"
MPV_SHA256="$(manifest_value mpv sha256)"
sha256_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | cut -d ' ' -f 1
else
shasum -a 256 "$1" | cut -d ' ' -f 1
fi
}
download_verified() {
local url="$1"
local expected_sha256="$2"
local destination="$3"
local temporary
local actual_sha256
if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ ]]; then
echo "Invalid SHA-256 pin for $url" >&2
return 1
fi
mkdir -p "$(dirname "$destination")"
temporary="$(mktemp "${destination}.tmp.XXXXXX")"
# Retries cover the transfer only. A checksum mismatch below is never retried:
# that is a tampered or moved artefact, not a flaky connection, and trying
# again would only turn a loud failure into an intermittent one.
if ! curl \
--fail \
--location \
--silent \
--show-error \
--retry 3 \
--retry-connrefused \
--retry-delay 5 \
--connect-timeout 30 \
--proto '=https,file' \
--tlsv1.2 \
--output "$temporary" \
"$url"; then
rm -f "$temporary"
return 1
fi
actual_sha256="$(sha256_file "$temporary")"
if [ "$actual_sha256" != "$expected_sha256" ]; then
echo "SHA-256 mismatch for $url" >&2
echo "Expected: $expected_sha256" >&2
echo "Actual: $actual_sha256" >&2
rm -f "$temporary" "$destination"
return 1
fi
mv "$temporary" "$destination"
}
checkout_verified_ref() {
local url="$1"
local ref="$2"
local expected_commit="$3"
local destination="$4"
local mirror="${5:-}"
local actual_commit
local source
local attempt
if [[ ! "$expected_commit" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid Git commit pin for $url at $ref" >&2
return 1
fi
# Retries and the mirror cover the transfer, never the verification. The commit
# pin below is checked identically whichever source answered, so a mirror can
# only supply the same tree or fail - it cannot substitute another one.
#
# This exists because code.videolan.org, the only source fetched over git,
# refused connections for well over two minutes at a time across several CI
# runs and took every build with it.
rm -rf "$destination"
for source in "$url" ${mirror:+"$mirror"}; do
for attempt in 1 2 3; do
if git clone --quiet --depth 1 --branch "$ref" --no-checkout \
"$source" "$destination"; then
break 2
fi
rm -rf "$destination"
# No point pausing before giving up on this source.
if [ "$attempt" -lt 3 ]; then sleep $((attempt * 5)); fi
done
echo "Could not clone $source at $ref after 3 attempts" >&2
done
if [ ! -d "$destination" ]; then
echo "No source produced $ref for $url" >&2
return 1
fi
actual_commit="$(git -C "$destination" rev-parse 'HEAD^{commit}')"
if [ "$actual_commit" != "$expected_commit" ]; then
echo "Git ref mismatch for $ref" >&2
echo "Expected: $expected_commit" >&2
echo "Actual: $actual_commit" >&2
rm -rf "$destination"
return 1
fi
git -C "$destination" checkout --quiet --detach "$expected_commit"
}
cleanup_srcdir=""
cleanup() {
if [ -n "$cleanup_srcdir" ]; then
rm -rf -- "$cleanup_srcdir"
fi
}
main() {
local prefix="${PREFIX:-$(pwd)/libmpv-prefix}"
local jobs="${JOBS:-$(nproc)}"
local srcdir
mkdir -p "$prefix"
prefix="$(realpath "$prefix")"
export PKG_CONFIG_PATH="$prefix/lib/pkgconfig:$prefix/lib/$(uname -m)-linux-gnu/pkgconfig:${PKG_CONFIG_PATH:-}"
srcdir="$(mktemp -d)"
cleanup_srcdir="$srcdir"
trap cleanup EXIT
cd "$srcdir"
echo "==> Sources in $srcdir"
echo "==> Install prefix: $prefix"
echo ""
# ─── Step 1: dav1d (static library) ────────────────────────────────────────
# The bundled ffmpeg has no AV1 software decoder: its native av1 decoder is
# hardware-accelerated only, and no libaom/libdav1d is linked in. When hwdec
# is unavailable or cannot serve the source (an AV1 file on a GPU without AV1
# decode), AV1 has no path at all - every packet fails, video hits EOF and
# the plane goes black while audio keeps playing. dav1d is the software
# floor under AV1, exactly as libass is for subtitles. It must come before
# ffmpeg, whose configure resolves --enable-libdav1d against dav1d's
# pkg-config file.
echo "==> Building dav1d $DAV1D_VERSION (static)..."
checkout_verified_ref \
"$DAV1D_URL" "$DAV1D_REF" "$DAV1D_COMMIT" \
"$srcdir/dav1d-v${DAV1D_VERSION}" "$DAV1D_MIRROR"
cd "dav1d-v${DAV1D_VERSION}"
meson setup build \
--prefix="$prefix" \
--default-library=static \
-Denable_tools=false \
-Denable_tests=false \
-Denable_examples=false \
-Denable_docs=false
ninja -C build -j"$jobs"
ninja -C build install
cd "$srcdir"
echo ""
echo "==> dav1d done."
echo ""
# ─── Step 2: ffmpeg (static libraries) ─────────────────────────────────────
echo "==> Building ffmpeg $FFMPEG_VERSION (static, decoder-only)..."
download_verified "$FFMPEG_URL" "$FFMPEG_SHA256" "$srcdir/ffmpeg.tar.xz"
tar -xJf "$srcdir/ffmpeg.tar.xz"
cd "ffmpeg-${FFMPEG_VERSION}"
./configure \
--prefix="$prefix" \
--enable-gpl \
--enable-version3 \
--enable-static \
--disable-shared \
--enable-pic \
--disable-programs \
--disable-doc \
--disable-encoders \
--disable-muxers \
--enable-muxer=spdif \
--disable-devices \
--disable-bsfs \
--enable-bsf=aac_adtstoasc,av1_metadata,extract_extradata,h264_metadata,h264_mp4toannexb,hevc_metadata,hevc_mp4toannexb,vp9_metadata \
--disable-filters \
--enable-filter=aformat,aresample,format,null,scale \
--enable-gnutls \
--enable-vaapi \
--enable-libdav1d \
--disable-vdpau \
--disable-debug \
--disable-stripping
make -j"$jobs"
make install
cd "$srcdir"
echo ""
echo "==> ffmpeg done."
echo ""
# ─── Step 3: shaderc (static library) ───────────────────────────────────────
echo "==> Building shaderc $SHADERC_VERSION (static)..."
checkout_verified_ref \
"$SHADERC_URL" "$SHADERC_REF" "$SHADERC_COMMIT" \
"$srcdir/shaderc-v${SHADERC_VERSION}"
cd "shaderc-v${SHADERC_VERSION}"
./utils/git-sync-deps
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$prefix" \
-DSHADERC_SKIP_TESTS=ON \
-DSHADERC_SKIP_EXAMPLES=ON \
-DSHADERC_SKIP_COPYRIGHT_CHECK=ON \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
cmake --build build -j"$jobs"
cmake --install build
cd "$srcdir"
echo ""
echo "==> shaderc done."
echo ""
# ─── Step 4: libplacebo (static library) ───────────────────────────────────
echo "==> Building libplacebo $LIBPLACEBO_VERSION (static)..."
checkout_verified_ref \
"$LIBPLACEBO_URL" "$LIBPLACEBO_REF" "$LIBPLACEBO_COMMIT" \
"$srcdir/libplacebo-v${LIBPLACEBO_VERSION}" "$LIBPLACEBO_MIRROR"
cd "libplacebo-v${LIBPLACEBO_VERSION}"
git submodule update --init --recursive
meson setup build \
--prefix="$prefix" \
--default-library=static \
-Dvulkan=disabled \
-Dd3d11=disabled \
-Ddemos=false \
-Dtests=false
ninja -C build -j"$jobs"
ninja -C build install
cd "$srcdir"
echo ""
echo "==> libplacebo done."
echo ""
# ─── Step 5: mpv (shared libmpv) ───────────────────────────────────────────
echo "==> Building mpv $MPV_VERSION (shared libmpv only)..."
download_verified "$MPV_URL" "$MPV_SHA256" "$srcdir/mpv.tar.gz"
tar -xzf "$srcdir/mpv.tar.gz"
cd "mpv-${MPV_VERSION}"
# The runner's only video path is a Wayland subsurface, and it hands mpv
# MPV_RENDER_PARAM_WL_DISPLAY so VAAPI can find the device instead of falling
# back to software decoding. A libmpv built without Wayland cannot use that.
# VDPAU goes with X11 - it has no Wayland backend at all.
#
# drm/vaapi-drm/egl are pinned enabled, not left on auto: mpv's `drm` feature
# silently drops to disabled when libdisplay-info is missing, and every VAAPI
# path that does not depend on a display server - vaapi-copy's standalone
# render-node device and the GL dmabuf interop - is derived from it. Shipping
# that build quietly lands every source on software decoding (the 2.13.0
# Fedora report). Enabled means the configure fails when the pieces are
# absent instead of degrading in silence.
meson setup build \
--prefix="$prefix" \
-Dlibmpv=true \
-Dcplayer=false \
-Dbuild-date=false \
-Dlua=enabled \
-Djavascript=enabled \
-Dcplugins=disabled \
-Dmanpage-build=disabled \
-Djack=disabled \
-Dvulkan=disabled \
-Dd3d11=disabled \
-Dgl=enabled \
-Degl=enabled \
-Ddrm=enabled \
-Dvaapi=enabled \
-Dvaapi-drm=enabled \
-Dvaapi-wayland=enabled \
-Dalsa=enabled \
-Dpulse=enabled \
-Dpipewire=enabled \
-Dvdpau=disabled \
-Dwayland=enabled \
-Dx11=disabled
ninja -C build -j"$jobs"
ninja -C build install
echo ""
echo "==> mpv done."
echo ""
echo "==> libmpv build complete. Output in $prefix"
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi