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
306 lines
12 KiB
Bash
Executable File
306 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=build-libmpv.sh
|
|
source "$SCRIPT_DIR/build-libmpv.sh"
|
|
|
|
fail() {
|
|
echo "FAIL: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
assert_absent() {
|
|
[ ! -e "$1" ] || fail "unexpected path remains: $1"
|
|
}
|
|
|
|
init_repository() {
|
|
mkdir -p "$1"
|
|
git -C "$1" init --quiet
|
|
git -C "$1" config user.name "Plezy provenance test"
|
|
git -C "$1" config user.email "provenance-test@invalid.example"
|
|
}
|
|
|
|
temporary="$(mktemp -d)"
|
|
trap 'rm -rf "$temporary"' EXIT
|
|
|
|
fixture="$temporary/source.bin"
|
|
destination="$temporary/download/output.bin"
|
|
printf 'reviewed native input\n' >"$fixture"
|
|
expected="$(sha256_file "$fixture")"
|
|
download_verified "file://$fixture" "$expected" "$destination"
|
|
cmp -s "$fixture" "$destination" || fail "verified download changed bytes"
|
|
|
|
printf 'reviewed native inpuu\n' >"$fixture"
|
|
rm -f "$destination"
|
|
if download_verified "file://$fixture" "$expected" "$destination"; then
|
|
fail "changed archive was accepted"
|
|
fi
|
|
assert_absent "$destination"
|
|
if compgen -G "$destination.tmp.*" >/dev/null; then
|
|
fail "failed download left a temporary file"
|
|
fi
|
|
|
|
repository="$temporary/repository"
|
|
checkout="$temporary/checkout"
|
|
init_repository "$repository"
|
|
printf 'first\n' >"$repository/input.txt"
|
|
git -C "$repository" add input.txt
|
|
git -C "$repository" commit --quiet -m first
|
|
git -C "$repository" tag release
|
|
approved_commit="$(git -C "$repository" rev-parse HEAD)"
|
|
checkout_verified_ref "file://$repository" release "$approved_commit" "$checkout"
|
|
[ "$(git -C "$checkout" rev-parse HEAD)" = "$approved_commit" ] ||
|
|
fail "verified checkout selected the wrong commit"
|
|
|
|
# A dead primary must fall through to the mirror, because the whole point is
|
|
# that one unreachable host cannot stop the build. This runs while the tag still
|
|
# points at the approved commit; the moved-tag case is below.
|
|
unreachable="file://$temporary/definitely-not-a-repository"
|
|
checkout_verified_ref "$unreachable" release "$approved_commit" "$checkout" "file://$repository"
|
|
[ "$(git -C "$checkout" rev-parse HEAD)" = "$approved_commit" ] ||
|
|
fail "mirror fallback selected the wrong commit"
|
|
|
|
# And the mirror answers to the same pin. A mirror serving a different tree is
|
|
# the one thing a fallback must never quietly accept.
|
|
mirror_repository="$temporary/mirror"
|
|
init_repository "$mirror_repository"
|
|
printf 'substituted\n' >"$mirror_repository/input.txt"
|
|
git -C "$mirror_repository" add input.txt
|
|
git -C "$mirror_repository" commit --quiet -m substituted
|
|
git -C "$mirror_repository" tag release
|
|
if checkout_verified_ref "$unreachable" release "$approved_commit" "$checkout" "file://$mirror_repository"; then
|
|
fail "mirror serving another commit was accepted"
|
|
fi
|
|
assert_absent "$checkout"
|
|
|
|
printf 'second\n' >"$repository/input.txt"
|
|
git -C "$repository" commit --quiet -am second
|
|
git -C "$repository" tag --force release >/dev/null
|
|
if checkout_verified_ref "file://$repository" release "$approved_commit" "$checkout"; then
|
|
fail "moved tag was accepted"
|
|
fi
|
|
assert_absent "$checkout"
|
|
|
|
# ─── The build plan ─────────────────────────────────────────────────────────
|
|
# Everything above checks how sources arrive. What they are then configured
|
|
# with decides whether the feature works at all, and it fails quietly: a libmpv
|
|
# built without -Dwayland=enabled still compiles, still links and still plays,
|
|
# it just has no Wayland backend, so the render context cannot accept
|
|
# MPV_RENDER_PARAM_WL_DISPLAY, vaapi finds no device and hardware decoding
|
|
# drops to a copy-back path. Nothing crashes, so nothing else notices. Run
|
|
# main() for real against stub build tools and assert the argument vectors they
|
|
# were handed.
|
|
|
|
stub_bin="$temporary/stub-bin"
|
|
stub_extra="$temporary/stub-extra"
|
|
records="$temporary/records"
|
|
mkdir -p "$stub_bin" "$stub_extra" "$records" "$temporary/tmp"
|
|
|
|
# Deliberately unlike the pinned versions: every path below is derived from the
|
|
# manifest, so a stub that matched by accident would prove nothing.
|
|
ffmpeg_version="9.9.9"
|
|
dav1d_version="2.2.2"
|
|
shaderc_version="6.6.6"
|
|
libplacebo_version="7.7.7"
|
|
mpv_version="8.8.8"
|
|
|
|
# Each stub records the vector it was called with, one argument per line, plus
|
|
# the directory it ran in - which is what tells mpv's meson call apart from
|
|
# libplacebo's. An argument containing a newline would corrupt the record, and
|
|
# none of these ever does: they are literal flags and mktemp -d paths.
|
|
make_stub() {
|
|
local directory="$1" name="$2" extra="${3:-}"
|
|
cat >"$directory/$name" <<STUB
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
record="\$(mktemp "$records/record.XXXXXX")"
|
|
{ printf '%s\n' "$name" "\$(pwd)"; [ \$# -eq 0 ] || printf '%s\n' "\$@"; } >"\$record"
|
|
$extra
|
|
STUB
|
|
chmod +x "$directory/$name"
|
|
}
|
|
|
|
payload="$temporary/payload.bin"
|
|
printf 'stub archive payload\n' >"$payload"
|
|
payload_sha256="$(sha256_file "$payload")"
|
|
|
|
curl_extra='
|
|
output=""
|
|
previous=""
|
|
for argument in "$@"; do
|
|
if [ "$previous" = "--output" ]; then output="$argument"; fi
|
|
previous="$argument"
|
|
done
|
|
[ -n "$output" ] || { echo "stub curl: no --output argument" >&2; exit 1; }
|
|
cp -- "'"$payload"'" "$output"
|
|
'
|
|
|
|
# The unmapped case is fatal on purpose: a new archive in the build plan has to
|
|
# come here and be described rather than silently extract to nothing.
|
|
tar_extra='
|
|
archive="$(basename -- "${!#}")"
|
|
case "$archive" in
|
|
ffmpeg.tar.xz) directory="ffmpeg-'"$ffmpeg_version"'" ;;
|
|
mpv.tar.gz) directory="mpv-'"$mpv_version"'" ;;
|
|
*) echo "stub tar: unexpected archive $archive" >&2; exit 1 ;;
|
|
esac
|
|
mkdir -p "$directory"
|
|
cp -- "'"$stub_extra"'/configure" "$directory/configure"
|
|
chmod +x "$directory/configure"
|
|
'
|
|
|
|
# ffmpeg runs ./configure out of its own tarball, so that one is planted by the
|
|
# tar stub rather than found on PATH.
|
|
make_stub "$stub_extra" configure
|
|
make_stub "$stub_bin" curl "$curl_extra"
|
|
make_stub "$stub_bin" tar "$tar_extra"
|
|
for tool in make cmake meson ninja; do
|
|
make_stub "$stub_bin" "$tool"
|
|
done
|
|
|
|
# git stays real, pointed at local repositories: the checkout is pinned by
|
|
# commit, and a stub that answered rev-parse would be asserting its own input.
|
|
shaderc_repository="$temporary/shaderc-source"
|
|
init_repository "$shaderc_repository"
|
|
mkdir -p "$shaderc_repository/utils"
|
|
printf '#!/bin/sh\nexit 0\n' >"$shaderc_repository/utils/git-sync-deps"
|
|
chmod +x "$shaderc_repository/utils/git-sync-deps"
|
|
git -C "$shaderc_repository" add utils/git-sync-deps
|
|
# Windows checkouts do not track the mode bit, and the build plan executes it.
|
|
git -C "$shaderc_repository" update-index --chmod=+x utils/git-sync-deps
|
|
git -C "$shaderc_repository" commit --quiet -m shaderc
|
|
git -C "$shaderc_repository" tag release
|
|
shaderc_commit="$(git -C "$shaderc_repository" rev-parse HEAD)"
|
|
|
|
dav1d_repository="$temporary/dav1d-source"
|
|
init_repository "$dav1d_repository"
|
|
printf 'stub dav1d\n' >"$dav1d_repository/meson.build"
|
|
git -C "$dav1d_repository" add meson.build
|
|
git -C "$dav1d_repository" commit --quiet -m dav1d
|
|
git -C "$dav1d_repository" tag release
|
|
dav1d_commit="$(git -C "$dav1d_repository" rev-parse HEAD)"
|
|
|
|
libplacebo_repository="$temporary/libplacebo-source"
|
|
init_repository "$libplacebo_repository"
|
|
printf 'stub libplacebo\n' >"$libplacebo_repository/meson.build"
|
|
git -C "$libplacebo_repository" add meson.build
|
|
git -C "$libplacebo_repository" commit --quiet -m libplacebo
|
|
git -C "$libplacebo_repository" tag release
|
|
libplacebo_commit="$(git -C "$libplacebo_repository" rev-parse HEAD)"
|
|
|
|
manifest="$temporary/native-inputs.json"
|
|
cat >"$manifest" <<JSON
|
|
{
|
|
"inputs": {
|
|
"ffmpeg": {
|
|
"version": "$ffmpeg_version",
|
|
"url": "https://stub.invalid/ffmpeg.tar.xz",
|
|
"sha256": "$payload_sha256"
|
|
},
|
|
"dav1d": {
|
|
"version": "$dav1d_version",
|
|
"url": "file://$dav1d_repository",
|
|
"ref": "release",
|
|
"commit": "$dav1d_commit"
|
|
},
|
|
"shaderc": {
|
|
"version": "$shaderc_version",
|
|
"url": "file://$shaderc_repository",
|
|
"ref": "release",
|
|
"commit": "$shaderc_commit"
|
|
},
|
|
"libplacebo": {
|
|
"version": "$libplacebo_version",
|
|
"url": "file://$libplacebo_repository",
|
|
"ref": "release",
|
|
"commit": "$libplacebo_commit"
|
|
},
|
|
"mpv": {
|
|
"version": "$mpv_version",
|
|
"url": "https://stub.invalid/mpv.tar.gz",
|
|
"sha256": "$payload_sha256"
|
|
}
|
|
}
|
|
}
|
|
JSON
|
|
|
|
# JOBS keeps nproc out of it; TMPDIR keeps main()'s own mktemp -d inside the
|
|
# directory this test already cleans up.
|
|
build_log="$temporary/build.log"
|
|
if ! (
|
|
export NATIVE_INPUTS_MANIFEST="$manifest"
|
|
export PATH="$stub_bin:$PATH"
|
|
export TMPDIR="$temporary/tmp"
|
|
export PREFIX="$temporary/prefix"
|
|
export JOBS=1
|
|
bash "$SCRIPT_DIR/build-libmpv.sh"
|
|
) >"$build_log" 2>&1; then
|
|
cat "$build_log" >&2
|
|
fail "the stubbed build plan did not run to completion"
|
|
fi
|
|
|
|
recorded_call() {
|
|
local program="$1" directory="$2" candidate
|
|
for candidate in "$records"/record.*; do
|
|
[ -f "$candidate" ] || continue
|
|
if [ "$(sed -n 1p "$candidate")" = "$program" ] &&
|
|
[ "$(basename -- "$(sed -n 2p "$candidate")")" = "$directory" ]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# -Fxq, so this matches a whole recorded argument. A substring search would
|
|
# accept -Dwayland=enabled inside a comment, which is exactly the hole here.
|
|
assert_argument() {
|
|
local record="$1" argument="$2" description="$3"
|
|
tail -n +3 "$record" | grep -Fxq -- "$argument" ||
|
|
fail "$description does not pass $argument"
|
|
}
|
|
|
|
mpv_meson="$(recorded_call meson "mpv-$mpv_version")" ||
|
|
fail "the build plan never ran meson in the mpv source tree"
|
|
[ "$(sed -n 3p "$mpv_meson")" = "setup" ] ||
|
|
fail "mpv's first meson call is no longer 'setup'"
|
|
|
|
assert_argument "$mpv_meson" "-Dwayland=enabled" "mpv's meson setup"
|
|
assert_argument "$mpv_meson" "-Dx11=disabled" "mpv's meson setup"
|
|
assert_argument "$mpv_meson" "-Dvaapi=enabled" "mpv's meson setup"
|
|
assert_argument "$mpv_meson" "-Dgl=enabled" "mpv's meson setup"
|
|
assert_argument "$mpv_meson" "-Degl=enabled" "mpv's meson setup"
|
|
|
|
# The DRM-derived providers are the ones that keep hardware decoding alive when
|
|
# the Wayland VA display cannot be had: vaapi-copy opens the render node on its
|
|
# own, and the GL dmabuf interop gives direct vaapi a display-independent
|
|
# path. mpv auto-disables the whole `drm` feature when libdisplay-info is
|
|
# missing, so these are pinned enabled and asserted here.
|
|
assert_argument "$mpv_meson" "-Ddrm=enabled" "mpv's meson setup"
|
|
assert_argument "$mpv_meson" "-Dvaapi-drm=enabled" "mpv's meson setup"
|
|
assert_argument "$mpv_meson" "-Dvaapi-wayland=enabled" "mpv's meson setup"
|
|
|
|
# vaapi has to reach ffmpeg too, or mpv's hwdec has no decoder behind it. And
|
|
# the bundled ffmpeg needs a software AV1 decoder: its native av1 codec is
|
|
# hardware-accelerated only, so without dav1d an AV1 source on a machine whose
|
|
# GPU cannot decode it fails every packet and plays black video with audio.
|
|
ffmpeg_configure="$(recorded_call configure "ffmpeg-$ffmpeg_version")" ||
|
|
fail "the build plan never configured ffmpeg"
|
|
assert_argument "$ffmpeg_configure" "--enable-vaapi" "ffmpeg's configure"
|
|
assert_argument "$ffmpeg_configure" "--enable-libdav1d" "ffmpeg's configure"
|
|
|
|
# dav1d must be built static so ffmpeg absorbs it into the bundled libmpv:
|
|
# a shared dav1d would have to travel in the bundle and be kept version-true
|
|
# with the host, which is exactly the coupling the rest of the plan avoids.
|
|
dav1d_meson="$(recorded_call meson "dav1d-v$dav1d_version")" ||
|
|
fail "the build plan never ran meson in the dav1d source tree"
|
|
assert_argument "$dav1d_meson" "--default-library=static" "dav1d's meson setup"
|
|
|
|
# libplacebo runs meson as well, and its call carries no Wayland flag: finding
|
|
# it separately is what proves the directory above really discriminated.
|
|
recorded_call meson "libplacebo-v$libplacebo_version" >/dev/null ||
|
|
fail "the build plan never ran meson in the libplacebo source tree"
|
|
|
|
echo "Linux native acquisition and build plan verification passed"
|