diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
index 62b91630c..5be91642f 100644
--- a/.github/actions/build/action.yml
+++ b/.github/actions/build/action.yml
@@ -50,8 +50,8 @@ runs:
uses: actions/cache/restore@v6
with:
path: ~/ffmpeg
- key: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt', 'webrtc-media/src/main/cpp/dependencies/ffmpeg/patches/*.patch') }}-${{ hashFiles('webrtc-media/pom.xml') }}
- restore-keys: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt', 'webrtc-media/src/main/cpp/dependencies/ffmpeg/patches/*.patch') }}-
+ key: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt', 'webrtc-media/src/main/cpp/dependencies/ffmpeg/patches/*.patch', 'webrtc-media/src/main/cpp/dependencies/libva/*') }}-${{ hashFiles('webrtc-media/pom.xml') }}
+ restore-keys: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt', 'webrtc-media/src/main/cpp/dependencies/ffmpeg/patches/*.patch', 'webrtc-media/src/main/cpp/dependencies/libva/*') }}-
# One cache per OS: the dependencies do not differ between the platforms of
# an OS. The project's own artifacts change with every build and stay out
diff --git a/.github/actions/prepare-linux/action.yml b/.github/actions/prepare-linux/action.yml
index d53992ff1..d9288b608 100644
--- a/.github/actions/prepare-linux/action.yml
+++ b/.github/actions/prepare-linux/action.yml
@@ -31,9 +31,11 @@ runs:
- name: Install required packages
run: |
sudo apt-get update -q
- # nasm is what FFmpeg assembles its x86 code with, and meson builds
- # dav1d, the software decoder of AV1 in the media module.
- sudo apt-get install -y -q binutils cmake git locales lsb-release meson nasm ninja-build pipewire pipewire-pulse pkg-config python3 python3-setuptools rsync unzip wget xz-utils
+ # nasm is what FFmpeg assembles its x86 code with, meson builds
+ # dav1d, the software decoder of AV1 in the media module, and
+ # libva-dev has the VA-API headers FFmpeg is built against, for every
+ # target.
+ sudo apt-get install -y -q binutils cmake git libva-dev locales lsb-release meson nasm ninja-build pipewire pipewire-pulse pkg-config python3 python3-setuptools rsync unzip wget xz-utils
shell: bash
# Chromium Clang to be used with the clang toolchain file. The JNI code
diff --git a/docs/guide/build.md b/docs/guide/build.md
index 0b299a4ea..48382c814 100644
--- a/docs/guide/build.md
+++ b/docs/guide/build.md
@@ -29,9 +29,11 @@ git submodule update --init --depth 1 webrtc-media/third-party/ffmpeg webrtc-med
Linux builds run on an x86-64 host and cross-compile for ARM. Install the build tools (Debian/Ubuntu package names):
```shell
-sudo apt-get install binutils cmake git make meson nasm ninja-build pkg-config python3 python3-setuptools rsync unzip wget xz-utils
+sudo apt-get install binutils cmake git libva-dev make meson nasm ninja-build pkg-config python3 python3-setuptools rsync unzip wget xz-utils
```
+`libva-dev` provides the VA-API headers FFmpeg is built against, for every target architecture, cross builds included. Only the headers are used: FFmpeg is linked against a small loader in `webrtc-media/src/main/cpp/dependencies/libva` instead of libva itself, so the libraries load on machines without libva. Set `-DLIBVA_INCLUDE_DIR=
` (the directory holding `va/`) for headers somewhere other than `/usr/include` or `/usr/local/include`.
+
The JNI library compiles against the libc++ headers of the WebRTC build. Those headers only support the Clang that the WebRTC branch pins, so the toolchain files expect that exact Clang in `/opt/clang`. Install it this way (the package name is in `.github/actions/prepare-linux/action.yml` and changes with the WebRTC branch):
```shell
diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md
index e0cc8e24b..16be04071 100644
--- a/docs/guide/media/media-files.md
+++ b/docs/guide/media/media-files.md
@@ -224,7 +224,7 @@ The pictures are the same ones software decoding gives; in the tests, every fram
**HEVC is decoded in hardware only.** A player decodes it on the media engine or GPU whether or not the flag is set, and never in software: `isHardwareDecoding()` is always `true` for it. Where the machine has no hardware decoder for HEVC, creating the player fails with an `IOException`; where the hardware does not take a stream (a profile it cannot decode) or fails while playing, playback stops with an error. The fallback below does not apply to it.
-- **Platforms:** macOS through VideoToolbox, Windows through Direct3D 12 (Windows 10 version 2004 or later, with a driver that decodes video through it) and otherwise Direct3D 11, and Linux (x86-64 and ARM64) through NVDEC on NVIDIA GPUs (the driver provides `libcuda.so.1` and `libnvcuvid.so.1`) and through Vulkan Video on the GPUs of Intel and AMD (the Vulkan loader `libvulkan.so.1` and a driver with the video extensions of the codec: Mesa's ANV or RADV, recent enough for each codec, AV1 and VP9 needing the newest). These libraries are loaded when a player asks for hardware, so a machine without them is not affected. The flag is accepted everywhere; where the platform has no hardware decoder, or the FFmpeg build has none for it (32-bit ARM), the video is decoded in software, except HEVC, and `isHardwareDecoding()` says so.
+- **Platforms:** macOS through VideoToolbox, Windows through Direct3D 12 (Windows 10 version 2004 or later, with a driver that decodes video through it) and otherwise Direct3D 11, and Linux (x86-64 and ARM64) through NVDEC on NVIDIA GPUs (the driver provides `libcuda.so.1` and `libnvcuvid.so.1`) and on the GPUs of Intel and AMD through VA-API (`libva.so.2` and `libva-drm.so.2` with a driver: Intel's `iHD` from intel-media-driver, or Mesa's `radeonsi`) and otherwise Vulkan Video (the Vulkan loader `libvulkan.so.1` and a driver with the video extensions of the codec: Mesa's ANV or RADV, recent enough for each codec, AV1 and VP9 needing the newest, which some distributions ship switched off). VA-API comes first because a Vulkan device can be created where its driver does not decode, and the player picks a device by whether it can be created. These libraries are loaded when a player asks for hardware, so a machine without them is not affected. The flag is accepted everywhere; where the platform has no hardware decoder, or the FFmpeg build has none for it (32-bit ARM), the video is decoded in software, except HEVC, and `isHardwareDecoding()` says so.
- **Codecs:** H.264, H.265/HEVC, VP9 and AV1, where the GPU decodes them: a GPU that is a few years old may lack AV1, and on a Mac only an M3 or later has it. VP8, MPEG-4 and MJPEG are decoded in software, and so is AV1 where there is no GPU decoder for it (it is decoded by dav1d, which is built into the FFmpeg libraries).
- **Fallback** (every codec but HEVC): a stream the hardware does not take, such as a profile it cannot decode, is decoded in software without the player noticing more than `isHardwareDecoding()` turning `false`. If the hardware fails before its first picture, the packets it was sent are decoded again in software and none is lost; if it fails after one, decoding goes on in software from the next key frame. A damaged packet is not taken for a hardware failure after the first picture: as with software decoding, playback stops with an error, and the hardware keeps the stream.
- **What it saves** is processor time, not the copy: a decoded picture is read back from the media engine into system memory, and converted to I420 as WebRTC wants it. On an Apple M2, a 1080p H.264 stream took 11.6 ms of processor time per frame in software and 1.4 ms in hardware, and a 4K stream 35.7 ms against 4.2 ms, about seven to eight times less. Per frame, software on all cores is faster on the clock, which does not matter at playback speed. Windows gains less: on an NVIDIA GeForce RTX 3080 through Direct3D 12, a 1080p H.264 stream took about 2.1 ms of processor time per frame against 3.6 ms in software, and a 4K stream 5.4 ms against 6.6 ms. Direct3D 11 reads the picture back more slowly and took more than software at 4K (12.4 ms), which is why Direct3D 12 comes first. These are figures for one machine each and for synthetic media, not a promise; try it on your own streams before relying on it.
diff --git a/webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt b/webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt
index 1723512f1..05dcdacb1 100644
--- a/webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt
+++ b/webrtc-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt
@@ -215,6 +215,24 @@ elseif(LINUX AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64|aarch64|ar
--enable-hwaccel=vp9_vulkan
--enable-hwaccel=av1_vulkan
)
+ # VA-API, the other way the GPUs of Intel and AMD decode, through the
+ # drivers distributions enable by default (Intel's iHD, Mesa's radeonsi),
+ # where Vulkan Video may still be switched off. FFmpeg links libva and
+ # libva-drm directly rather than loading them, so it is given a stand-in
+ # for them instead, dependencies/libva/va_loader.c, which loads libva with
+ # dlopen on its first call: the libraries load on a machine without libva
+ # as they do without CUDA or Vulkan. The build needs the libva headers of
+ # the host (libva-dev), found below. The display is opened on a DRM render
+ # node; neither X11 nor libdrm is needed.
+ set(FFMPEG_VAAPI TRUE)
+
+ list(APPEND FFMPEG_COMPONENTS
+ --enable-vaapi
+ --enable-hwaccel=h264_vaapi
+ --enable-hwaccel=hevc_vaapi
+ --enable-hwaccel=vp9_vaapi
+ --enable-hwaccel=av1_vaapi
+ )
endif()
# The parent adds the licenses of the NVDEC and Vulkan headers to the platform
@@ -250,6 +268,50 @@ foreach(PATCH ${FFMPEG_PATCHES})
string(APPEND FFMPEG_COMPONENTS_TEXT "\npatch ${PATCH_NAME} ${PATCH_HASH}")
endforeach()
+#
+# The libva headers, and the stand-in for libva that FFmpeg is linked against.
+# Both belong to the install: another version of the headers changes what
+# configure finds in them, and the stand-in is linked into the libraries.
+#
+if(FFMPEG_VAAPI)
+ # The host's headers, from libva-dev, and only those: a sysroot may have
+ # headers of its own, older ones, and which of them a search turned up
+ # would depend on the toolchain. They are the same for every architecture.
+ find_path(LIBVA_INCLUDE_DIR
+ NAMES va/va.h va/va_drm.h
+ PATHS /usr/include /usr/local/include
+ NO_DEFAULT_PATH
+ NO_CMAKE_FIND_ROOT_PATH)
+
+ if(NOT LIBVA_INCLUDE_DIR)
+ message(FATAL_ERROR
+ "No libva headers (va/va.h, va/va_drm.h), which FFmpeg's VA-API decoders are built "
+ "against. Install them, for example with: sudo apt-get install libva-dev, or set "
+ "LIBVA_INCLUDE_DIR to the directory that holds va/.")
+ endif()
+
+ file(STRINGS "${LIBVA_INCLUDE_DIR}/va/va_version.h" LIBVA_VERSION_LINES
+ REGEX "^#define VA_(MAJOR|MINOR|MICRO)_VERSION +[0-9]+")
+
+ foreach(PART MAJOR MINOR MICRO)
+ if(LIBVA_VERSION_LINES MATCHES "VA_${PART}_VERSION +([0-9]+)")
+ set(LIBVA_${PART} "${CMAKE_MATCH_1}")
+ else()
+ message(FATAL_ERROR "No VA_${PART}_VERSION in ${LIBVA_INCLUDE_DIR}/va/va_version.h")
+ endif()
+ endforeach()
+
+ # The version of the API, which is what libva.pc gives as its version and
+ # what configure compares against (libva >= 0.35.0).
+ set(LIBVA_API_VERSION "${LIBVA_MAJOR}.${LIBVA_MINOR}.${LIBVA_MICRO}")
+
+ get_filename_component(LIBVA_LOADER_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libva" ABSOLUTE)
+ file(SHA256 "${LIBVA_LOADER_DIR}/va_loader.c" LIBVA_LOADER_HASH)
+
+ string(APPEND FFMPEG_COMPONENTS_TEXT
+ "\nlibva headers ${LIBVA_API_VERSION} loader ${LIBVA_LOADER_HASH}")
+endif()
+
#
# dav1d is the software decoder of AV1, built from the third-party/dav1d
# submodule with Meson into a prefix of its own, as a static library, which
@@ -410,6 +472,52 @@ function(ffmpeg_is_installed RESULT)
endif()
endfunction()
+#
+# Checks that the libraries just built take libva from the stand-in: none of
+# them may name libva as a dependency, which a pkg-config file of the host
+# found ahead of ours would make them do, and none may leave a function of it
+# undefined, which a function FFmpeg calls and the stand-in lacks would.
+# Either way they would not load without libva, or would fail on the call.
+#
+function(ffmpeg_check_libva_loaded_at_run_time)
+ find_program(READELF_EXECUTABLE NAMES ${CMAKE_READELF} readelf)
+
+ if(NOT READELF_EXECUTABLE)
+ message(WARNING "No readelf, so the FFmpeg libraries are not checked for a dependency on libva")
+ return()
+ endif()
+
+ file(GLOB INSTALLED_LIBS "${FFMPEG_INSTALL_DIR}/lib/*.so.*")
+
+ foreach(LIB ${INSTALLED_LIBS})
+ if(IS_SYMLINK "${LIB}")
+ continue()
+ endif()
+
+ execute_process(
+ COMMAND "${READELF_EXECUTABLE}" --dynamic --dyn-syms --wide "${LIB}"
+ OUTPUT_VARIABLE ELF_INFO
+ RESULT_VARIABLE ELF_RESULT
+ ERROR_QUIET)
+
+ if(NOT ELF_RESULT EQUAL 0)
+ message(FATAL_ERROR "readelf could not read ${LIB}")
+ endif()
+
+ if(ELF_INFO MATCHES "Shared library: \\[(libva[-.a-z0-9]*)\\]")
+ message(FATAL_ERROR
+ "${LIB} depends on ${CMAKE_MATCH_1}: FFmpeg was linked against the libva of the "
+ "host instead of the stand-in in ${LIBVA_LOADER_DIR}.")
+ endif()
+
+ if(ELF_INFO MATCHES "UND (va[A-Z][A-Za-z0-9_]*)")
+ message(FATAL_ERROR
+ "${LIB} leaves ${CMAKE_MATCH_1} undefined, a function of libva that FFmpeg calls "
+ "and ${LIBVA_LOADER_DIR}/va_loader.c does not define. Add it there.")
+ endif()
+ endforeach()
+endfunction()
+
ffmpeg_is_installed(FFMPEG_INSTALLED)
if(FFMPEG_INSTALLED)
@@ -894,16 +1002,16 @@ fi
set(FFMPEG_MAKE "${MAKE_EXECUTABLE}")
endif()
- if(FFMPEG_NVDEC OR FFMPEG_VULKAN)
- # configure looks for the NVDEC and Vulkan headers through pkg-config,
- # so that needs to be there, and to know where the vendored headers
+ if(FFMPEG_NVDEC OR FFMPEG_VULKAN OR FFMPEG_VAAPI)
+ # configure looks for the NVDEC, Vulkan and VA-API headers through
+ # pkg-config, so that needs to be there, and to know where the headers
# are.
find_program(PKG_CONFIG_EXECUTABLE NAMES pkg-config pkgconf)
if(NOT PKG_CONFIG_EXECUTABLE)
message(FATAL_ERROR
- "No pkg-config, which FFmpeg's configure uses to find the NVDEC and Vulkan headers. "
- "Install it, for example with: sudo apt-get install pkg-config")
+ "No pkg-config, which FFmpeg's configure uses to find the NVDEC, Vulkan and VA-API "
+ "headers. Install it, for example with: sudo apt-get install pkg-config")
endif()
set(HEADERS_PC_DIR "${CMAKE_CURRENT_BINARY_DIR}/headers-pc")
@@ -918,6 +1026,21 @@ fi
configure_file("${VULKAN_HEADERS_DIR}/vulkan.pc.in" "${HEADERS_PC_DIR}/vulkan.pc" @ONLY)
endif()
+ if(FFMPEG_VAAPI)
+ # Only the va/ headers, copied, so that the include directory
+ # configure is given holds nothing else of the host's: on a cross
+ # build, -I/usr/include would put the host's C library in front
+ # of the target's.
+ set(LIBVA_STAGED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libva-include")
+ set(LIBVA_LOADER_LIB_DIR "${CMAKE_CURRENT_BINARY_DIR}/libva-loader")
+
+ file(REMOVE_RECURSE "${LIBVA_STAGED_INCLUDE_DIR}" "${LIBVA_LOADER_LIB_DIR}")
+ file(COPY "${LIBVA_INCLUDE_DIR}/va" DESTINATION "${LIBVA_STAGED_INCLUDE_DIR}")
+ file(MAKE_DIRECTORY "${LIBVA_LOADER_LIB_DIR}")
+
+ configure_file("${LIBVA_LOADER_DIR}/libva.pc.in" "${HEADERS_PC_DIR}/libva.pc" @ONLY)
+ configure_file("${LIBVA_LOADER_DIR}/libva-drm.pc.in" "${HEADERS_PC_DIR}/libva-drm.pc" @ONLY)
+ endif()
# Only our directory: a sysroot or library path of the host would put
# the includedir somewhere it is not, and the host's own vulkan.pc
@@ -928,12 +1051,37 @@ unset PKG_CONFIG_SYSROOT_DIR PKG_CONFIG_LIBDIR
")
endif()
+ # The stand-in for libva is built for the target as FFmpeg is, with the
+ # same compiler and flags, into the static library libva.pc names.
+ # Position-independent, as it goes into shared libraries, and hidden, so
+ # that they do not export it.
+ set(LIBVA_LOADER_STEPS "")
+
+ if(FFMPEG_VAAPI)
+ set(LIBVA_LOADER_CFLAGS "${FFMPEG_EXTRA_CFLAGS}")
+
+ if(CMAKE_SYSROOT)
+ string(APPEND LIBVA_LOADER_CFLAGS " --sysroot=${CMAKE_SYSROOT}")
+ endif()
+
+ if(CMAKE_AR)
+ set(LIBVA_LOADER_AR "${CMAKE_AR}")
+ else()
+ set(LIBVA_LOADER_AR ar)
+ endif()
+
+ set(LIBVA_LOADER_STEPS
+"\"${CMAKE_C_COMPILER}\" ${LIBVA_LOADER_CFLAGS} -O2 -fPIC -fvisibility=hidden -I\"${LIBVA_STAGED_INCLUDE_DIR}\" -c \"${LIBVA_LOADER_DIR}/va_loader.c\" -o \"${LIBVA_LOADER_LIB_DIR}/va_loader.o\"
+\"${LIBVA_LOADER_AR}\" rcs \"${LIBVA_LOADER_LIB_DIR}/libva-loader.a\" \"${LIBVA_LOADER_LIB_DIR}/va_loader.o\"
+")
+ endif()
+
set(FFMPEG_BUILD_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/build-ffmpeg.sh")
file(WRITE "${FFMPEG_BUILD_SCRIPT}"
"# Generated by CMake. Builds FFmpeg ${FFMPEG_VERSION} for webrtc-java-media.
set -e
-${FFMPEG_SHELL_PREAMBLE}${DAV1D_BUILD_STEPS}cd \"${FFMPEG_BUILD_DIR_SH}\"
+${FFMPEG_SHELL_PREAMBLE}${DAV1D_BUILD_STEPS}${LIBVA_LOADER_STEPS}cd \"${FFMPEG_BUILD_DIR_SH}\"
# FFmpeg names its version after the git tag it finds, and a shallow clone of
# the submodule has none, so it would name itself after a commit hash. Handing
# make the release the source says it is names it the same way however the
@@ -1016,6 +1164,10 @@ set MSYS2_PATH_TYPE=inherit
"${FFMPEG_BUILD_SCRIPT}.")
endif()
+ if(FFMPEG_VAAPI)
+ ffmpeg_check_libva_loaded_at_run_time()
+ endif()
+
file(WRITE "${FFMPEG_COMPONENTS_FILE}" "${FFMPEG_COMPONENTS_TEXT}")
ffmpeg_is_installed(FFMPEG_INSTALLED)
diff --git a/webrtc-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in b/webrtc-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in
index 7ec608662..6003b9b90 100644
--- a/webrtc-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in
+++ b/webrtc-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in
@@ -26,6 +26,16 @@ META-INF/licenses/dav1d/COPYING of this jar. It is built from the release tag
https://code.videolan.org/videolan/dav1d/-/tree/@DAV1D_VERSION@
+libva loader
+------------
+
+In the Linux platform jars for x86-64 and ARM64, libavutil and libavcodec
+contain a small loader for libva (VA-API), part of webrtc-java and under its
+license, linked in statically in place of libva itself. It loads the libva
+installed on the machine, if there is one, when a player first asks for VA-API.
+libva is not part of this jar, and the libraries do not need it to load.
+
+
Source code
-----------
@@ -57,7 +67,7 @@ HEVC/H.265
Disclaimer regarding HEVC/H.265:
This project decodes HEVC/H.265 video through hardware acceleration only, using
the hardware decoders of the operating system or GPU (VideoToolbox, Direct3D
-11/12, NVDEC, Vulkan Video). It does not include, bundle, or distribute any
+11/12, NVDEC, VA-API, Vulkan Video). It does not include, bundle, or distribute any
software-based HEVC encoder (such as x265), and it never decodes HEVC in
software. The FFmpeg libraries in the webrtc-java-media platform jars that
decode HEVC in hardware (all but 32-bit ARM Linux) contain FFmpeg's HEVC
diff --git a/webrtc-media/src/main/cpp/dependencies/libva/README.md b/webrtc-media/src/main/cpp/dependencies/libva/README.md
new file mode 100644
index 000000000..2e2266ddb
--- /dev/null
+++ b/webrtc-media/src/main/cpp/dependencies/libva/README.md
@@ -0,0 +1,27 @@
+# libva loader
+
+FFmpeg decodes with VA-API on Linux (the `h264_vaapi`, `hevc_vaapi`, `vp9_vaapi` and `av1_vaapi`
+hwaccels), which is how the GPUs of Intel and AMD decode through the drivers distributions enable by
+default: Intel's `iHD` (intel-media-driver) and Mesa's `radeonsi`.
+
+Unlike CUDA and Vulkan, which FFmpeg loads with `dlopen`, it links libva and libva-drm directly.
+The FFmpeg libraries would then name `libva.so.2` and `libva-drm.so.2` as dependencies, and a
+machine without libva could not load them at all. So FFmpeg is linked against `va_loader.c`
+instead: a static library that defines every libva function FFmpeg calls and loads the real libva
+with `dlopen` on the first call. Where there is none, `vaGetDisplayDRM` returns `NULL`, FFmpeg
+creates no device, and the player goes on to the next kind of device or decodes in software.
+
+The headers are the host's, from the `libva-dev` package (`sudo apt-get install libva-dev`), and
+the same for every architecture. CMake copies the `va/` directory into the build, so that a cross
+build never gets `-I/usr/include`, and fills in `libva.pc.in` and `libva-drm.pc.in`, which FFmpeg's
+configure finds ahead of the host's own. Set `LIBVA_INCLUDE_DIR` for headers elsewhere. A libva
+older than the headers is fine: a function it lacks returns `VA_STATUS_ERROR_UNIMPLEMENTED`,
+except `vaMapBuffer2` (libva 2.21), which falls back to `vaMapBuffer`.
+
+After the build, CMake checks with `readelf` that no FFmpeg library depends on libva, and that none
+leaves a `va*` function undefined. A new FFmpeg release that calls a function this file does not
+define fails that check: add the function to `va_loader.c`. The pointer types come from the
+headers' declarations, so the compiler holds each function to its prototype.
+
+The version of the headers and a hash of `va_loader.c` are recorded in `components.txt`, so a
+change to either rebuilds FFmpeg.
diff --git a/webrtc-media/src/main/cpp/dependencies/libva/libva-drm.pc.in b/webrtc-media/src/main/cpp/dependencies/libva/libva-drm.pc.in
new file mode 100644
index 000000000..4731025c0
--- /dev/null
+++ b/webrtc-media/src/main/cpp/dependencies/libva/libva-drm.pc.in
@@ -0,0 +1,7 @@
+includedir=@LIBVA_STAGED_INCLUDE_DIR@
+
+Name: libva-drm
+Description: The libva DRM headers of the host; vaGetDisplayDRM is in the stand-in libva.pc names
+Version: @LIBVA_API_VERSION@
+Requires: libva
+Cflags: -I${includedir}
diff --git a/webrtc-media/src/main/cpp/dependencies/libva/libva.pc.in b/webrtc-media/src/main/cpp/dependencies/libva/libva.pc.in
new file mode 100644
index 000000000..a741b20a5
--- /dev/null
+++ b/webrtc-media/src/main/cpp/dependencies/libva/libva.pc.in
@@ -0,0 +1,8 @@
+includedir=@LIBVA_STAGED_INCLUDE_DIR@
+libdir=@LIBVA_LOADER_LIB_DIR@
+
+Name: libva
+Description: The libva headers of the host, with a stand-in that loads libva at run time
+Version: @LIBVA_API_VERSION@
+Cflags: -I${includedir}
+Libs: -L${libdir} -lva-loader -ldl -lpthread
diff --git a/webrtc-media/src/main/cpp/dependencies/libva/va_loader.c b/webrtc-media/src/main/cpp/dependencies/libva/va_loader.c
new file mode 100644
index 000000000..44d18f30b
--- /dev/null
+++ b/webrtc-media/src/main/cpp/dependencies/libva/va_loader.c
@@ -0,0 +1,402 @@
+/*
+ * Copyright 2026 Alex Andres
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Stands in for libva and libva-drm when FFmpeg is linked, so that the FFmpeg
+ * libraries do not need them to load: every function FFmpeg calls is defined
+ * here, and loads the real libva with dlopen on its first call.
+ *
+ * Linked against the real libraries, libavutil and libavcodec would name them
+ * as dependencies, and a machine without libva could not load them at all,
+ * hardware decoding or not. This way such a machine finds no VA display
+ * (vaGetDisplayDRM returns NULL), the device is not created, and the player
+ * goes on to the next kind of device or to software, as it does where there
+ * is no CUDA.
+ *
+ * The functions are the ones FFmpeg's VA-API device and decoders call, as
+ * nm -u lists them for the objects of its build, and vaMapBuffer, which it
+ * calls instead of vaMapBuffer2 with headers older than libva 2.21. A function
+ * that is missing here leaves an undefined symbol in the FFmpeg libraries,
+ * which the build checks for.
+ *
+ * The declarations come from the libva headers the build is given, and every
+ * pointer here takes its type from them, so the compiler holds each function
+ * to its prototype.
+ */
+
+#include
+#include
+#include
+
+#include
+#include
+
+/* The libraries of libva 2.x, whose ABI the soname promises to keep. */
+#define LIBVA_NAME "libva.so.2"
+#define LIBVA_DRM_NAME "libva-drm.so.2"
+
+#if VA_CHECK_VERSION(1, 21, 0)
+#define LOADER_HAS_MAP_BUFFER2 1
+#else
+#define LOADER_HAS_MAP_BUFFER2 0
+#endif
+
+#define LIBVA_FUNCTIONS(X) \
+ X(vaBeginPicture) \
+ X(vaCreateBuffer) \
+ X(vaCreateConfig) \
+ X(vaCreateContext) \
+ X(vaCreateImage) \
+ X(vaCreateSurfaces) \
+ X(vaDeriveImage) \
+ X(vaDestroyBuffer) \
+ X(vaDestroyConfig) \
+ X(vaDestroyContext) \
+ X(vaDestroyImage) \
+ X(vaDestroySurfaces) \
+ X(vaEndPicture) \
+ X(vaErrorStr) \
+ X(vaGetDisplayAttributes) \
+ X(vaGetImage) \
+ X(vaInitialize) \
+ X(vaMapBuffer) \
+ X(vaMaxNumImageFormats) \
+ X(vaMaxNumProfiles) \
+ X(vaPutImage) \
+ X(vaQueryConfigProfiles) \
+ X(vaQueryImageFormats) \
+ X(vaQuerySurfaceAttributes) \
+ X(vaQueryVendorString) \
+ X(vaRenderPicture) \
+ X(vaSetDriverName) \
+ X(vaSetErrorCallback) \
+ X(vaSetInfoCallback) \
+ X(vaSyncSurface) \
+ X(vaTerminate) \
+ X(vaUnmapBuffer)
+
+#define LIBVA_POINTER(name) static __typeof__(name) * real_##name;
+
+LIBVA_FUNCTIONS(LIBVA_POINTER)
+
+#if LOADER_HAS_MAP_BUFFER2
+LIBVA_POINTER(vaMapBuffer2)
+#endif
+
+LIBVA_POINTER(vaGetDisplayDRM)
+
+static pthread_once_t load_once = PTHREAD_ONCE_INIT;
+
+static void load(void)
+{
+ /* Kept open for the life of the process: the display and everything made
+ * on it belong to the library. Local, so that its symbols do not stand in
+ * for these or for anyone else's. */
+ void * libva = dlopen(LIBVA_NAME, RTLD_NOW | RTLD_LOCAL);
+
+ if (libva == NULL) {
+ return;
+ }
+
+ void * libva_drm = dlopen(LIBVA_DRM_NAME, RTLD_NOW | RTLD_LOCAL);
+
+ if (libva_drm == NULL) {
+ /* No display to be had without it, so none of libva is used. */
+ dlclose(libva);
+
+ return;
+ }
+
+#define LIBVA_RESOLVE(name) real_##name = (__typeof__(name) *) dlsym(libva, #name);
+
+ LIBVA_FUNCTIONS(LIBVA_RESOLVE)
+
+#if LOADER_HAS_MAP_BUFFER2
+ /* libva 2.21 and later; an older one has vaMapBuffer only, which the
+ * stand-in below falls back to. */
+ LIBVA_RESOLVE(vaMapBuffer2)
+#endif
+
+ real_vaGetDisplayDRM = (__typeof__(vaGetDisplayDRM) *) dlsym(libva_drm, "vaGetDisplayDRM");
+}
+
+#define LOAD_OR_RETURN(name, fallback) \
+ pthread_once(&load_once, load); \
+ if (real_##name == NULL) { \
+ return fallback; \
+ }
+
+VADisplay vaGetDisplayDRM(int fd)
+{
+ /* NULL is how libva says that there is no display on the device, which is
+ * what FFmpeg makes of a machine without libva as well. */
+ LOAD_OR_RETURN(vaGetDisplayDRM, NULL)
+
+ return real_vaGetDisplayDRM(fd);
+}
+
+VAStatus vaInitialize(VADisplay dpy, int * major_version, int * minor_version)
+{
+ LOAD_OR_RETURN(vaInitialize, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaInitialize(dpy, major_version, minor_version);
+}
+
+VAStatus vaTerminate(VADisplay dpy)
+{
+ LOAD_OR_RETURN(vaTerminate, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaTerminate(dpy);
+}
+
+const char * vaErrorStr(VAStatus error_status)
+{
+ LOAD_OR_RETURN(vaErrorStr, "libva is not available")
+
+ return real_vaErrorStr(error_status);
+}
+
+const char * vaQueryVendorString(VADisplay dpy)
+{
+ LOAD_OR_RETURN(vaQueryVendorString, NULL)
+
+ return real_vaQueryVendorString(dpy);
+}
+
+VAStatus vaSetDriverName(VADisplay dpy, char * driver_name)
+{
+ LOAD_OR_RETURN(vaSetDriverName, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaSetDriverName(dpy, driver_name);
+}
+
+VAMessageCallback vaSetErrorCallback(VADisplay dpy, VAMessageCallback callback, void * user_context)
+{
+ LOAD_OR_RETURN(vaSetErrorCallback, NULL)
+
+ return real_vaSetErrorCallback(dpy, callback, user_context);
+}
+
+VAMessageCallback vaSetInfoCallback(VADisplay dpy, VAMessageCallback callback, void * user_context)
+{
+ LOAD_OR_RETURN(vaSetInfoCallback, NULL)
+
+ return real_vaSetInfoCallback(dpy, callback, user_context);
+}
+
+VAStatus vaGetDisplayAttributes(VADisplay dpy, VADisplayAttribute * attr_list, int num_attributes)
+{
+ LOAD_OR_RETURN(vaGetDisplayAttributes, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaGetDisplayAttributes(dpy, attr_list, num_attributes);
+}
+
+int vaMaxNumProfiles(VADisplay dpy)
+{
+ LOAD_OR_RETURN(vaMaxNumProfiles, 0)
+
+ return real_vaMaxNumProfiles(dpy);
+}
+
+int vaMaxNumImageFormats(VADisplay dpy)
+{
+ LOAD_OR_RETURN(vaMaxNumImageFormats, 0)
+
+ return real_vaMaxNumImageFormats(dpy);
+}
+
+VAStatus vaQueryConfigProfiles(VADisplay dpy, VAProfile * profile_list, int * num_profiles)
+{
+ LOAD_OR_RETURN(vaQueryConfigProfiles, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaQueryConfigProfiles(dpy, profile_list, num_profiles);
+}
+
+VAStatus vaQueryImageFormats(VADisplay dpy, VAImageFormat * format_list, int * num_formats)
+{
+ LOAD_OR_RETURN(vaQueryImageFormats, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaQueryImageFormats(dpy, format_list, num_formats);
+}
+
+VAStatus vaQuerySurfaceAttributes(VADisplay dpy, VAConfigID config, VASurfaceAttrib * attrib_list,
+ unsigned int * num_attribs)
+{
+ LOAD_OR_RETURN(vaQuerySurfaceAttributes, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaQuerySurfaceAttributes(dpy, config, attrib_list, num_attribs);
+}
+
+VAStatus vaCreateConfig(VADisplay dpy, VAProfile profile, VAEntrypoint entrypoint,
+ VAConfigAttrib * attrib_list, int num_attribs, VAConfigID * config_id)
+{
+ LOAD_OR_RETURN(vaCreateConfig, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaCreateConfig(dpy, profile, entrypoint, attrib_list, num_attribs, config_id);
+}
+
+VAStatus vaDestroyConfig(VADisplay dpy, VAConfigID config_id)
+{
+ LOAD_OR_RETURN(vaDestroyConfig, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaDestroyConfig(dpy, config_id);
+}
+
+VAStatus vaCreateSurfaces(VADisplay dpy, unsigned int format, unsigned int width, unsigned int height,
+ VASurfaceID * surfaces, unsigned int num_surfaces, VASurfaceAttrib * attrib_list,
+ unsigned int num_attribs)
+{
+ LOAD_OR_RETURN(vaCreateSurfaces, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaCreateSurfaces(dpy, format, width, height, surfaces, num_surfaces, attrib_list,
+ num_attribs);
+}
+
+VAStatus vaDestroySurfaces(VADisplay dpy, VASurfaceID * surfaces, int num_surfaces)
+{
+ LOAD_OR_RETURN(vaDestroySurfaces, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaDestroySurfaces(dpy, surfaces, num_surfaces);
+}
+
+VAStatus vaCreateContext(VADisplay dpy, VAConfigID config_id, int picture_width, int picture_height,
+ int flag, VASurfaceID * render_targets, int num_render_targets, VAContextID * context)
+{
+ LOAD_OR_RETURN(vaCreateContext, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaCreateContext(dpy, config_id, picture_width, picture_height, flag, render_targets,
+ num_render_targets, context);
+}
+
+VAStatus vaDestroyContext(VADisplay dpy, VAContextID context)
+{
+ LOAD_OR_RETURN(vaDestroyContext, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaDestroyContext(dpy, context);
+}
+
+VAStatus vaCreateBuffer(VADisplay dpy, VAContextID context, VABufferType type, unsigned int size,
+ unsigned int num_elements, void * data, VABufferID * buf_id)
+{
+ LOAD_OR_RETURN(vaCreateBuffer, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaCreateBuffer(dpy, context, type, size, num_elements, data, buf_id);
+}
+
+VAStatus vaDestroyBuffer(VADisplay dpy, VABufferID buffer_id)
+{
+ LOAD_OR_RETURN(vaDestroyBuffer, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaDestroyBuffer(dpy, buffer_id);
+}
+
+VAStatus vaMapBuffer(VADisplay dpy, VABufferID buf_id, void ** pbuf)
+{
+ LOAD_OR_RETURN(vaMapBuffer, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaMapBuffer(dpy, buf_id, pbuf);
+}
+
+#if LOADER_HAS_MAP_BUFFER2
+VAStatus vaMapBuffer2(VADisplay dpy, VABufferID buf_id, void ** pbuf, uint32_t flags)
+{
+ pthread_once(&load_once, load);
+
+ if (real_vaMapBuffer2 == NULL) {
+ /* The flags only say how the mapping is going to be used, which an
+ * older libva does not ask about. */
+ (void) flags;
+
+ return vaMapBuffer(dpy, buf_id, pbuf);
+ }
+
+ return real_vaMapBuffer2(dpy, buf_id, pbuf, flags);
+}
+#endif
+
+VAStatus vaUnmapBuffer(VADisplay dpy, VABufferID buf_id)
+{
+ LOAD_OR_RETURN(vaUnmapBuffer, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaUnmapBuffer(dpy, buf_id);
+}
+
+VAStatus vaBeginPicture(VADisplay dpy, VAContextID context, VASurfaceID render_target)
+{
+ LOAD_OR_RETURN(vaBeginPicture, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaBeginPicture(dpy, context, render_target);
+}
+
+VAStatus vaRenderPicture(VADisplay dpy, VAContextID context, VABufferID * buffers, int num_buffers)
+{
+ LOAD_OR_RETURN(vaRenderPicture, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaRenderPicture(dpy, context, buffers, num_buffers);
+}
+
+VAStatus vaEndPicture(VADisplay dpy, VAContextID context)
+{
+ LOAD_OR_RETURN(vaEndPicture, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaEndPicture(dpy, context);
+}
+
+VAStatus vaSyncSurface(VADisplay dpy, VASurfaceID render_target)
+{
+ LOAD_OR_RETURN(vaSyncSurface, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaSyncSurface(dpy, render_target);
+}
+
+VAStatus vaCreateImage(VADisplay dpy, VAImageFormat * format, int width, int height, VAImage * image)
+{
+ LOAD_OR_RETURN(vaCreateImage, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaCreateImage(dpy, format, width, height, image);
+}
+
+VAStatus vaDestroyImage(VADisplay dpy, VAImageID image)
+{
+ LOAD_OR_RETURN(vaDestroyImage, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaDestroyImage(dpy, image);
+}
+
+VAStatus vaDeriveImage(VADisplay dpy, VASurfaceID surface, VAImage * image)
+{
+ LOAD_OR_RETURN(vaDeriveImage, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaDeriveImage(dpy, surface, image);
+}
+
+VAStatus vaGetImage(VADisplay dpy, VASurfaceID surface, int x, int y, unsigned int width,
+ unsigned int height, VAImageID image)
+{
+ LOAD_OR_RETURN(vaGetImage, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaGetImage(dpy, surface, x, y, width, height, image);
+}
+
+VAStatus vaPutImage(VADisplay dpy, VASurfaceID surface, VAImageID image, int src_x, int src_y,
+ unsigned int src_width, unsigned int src_height, int dest_x, int dest_y,
+ unsigned int dest_width, unsigned int dest_height)
+{
+ LOAD_OR_RETURN(vaPutImage, VA_STATUS_ERROR_UNIMPLEMENTED)
+
+ return real_vaPutImage(dpy, surface, image, src_x, src_y, src_width, src_height, dest_x, dest_y,
+ dest_width, dest_height);
+}
diff --git a/webrtc-media/src/main/cpp/src/media/VideoDecoder.cpp b/webrtc-media/src/main/cpp/src/media/VideoDecoder.cpp
index cbf47d30d..bf0233cb4 100644
--- a/webrtc-media/src/main/cpp/src/media/VideoDecoder.cpp
+++ b/webrtc-media/src/main/cpp/src/media/VideoDecoder.cpp
@@ -44,11 +44,16 @@ namespace
#elif defined(_WIN32)
return { AV_HWDEVICE_TYPE_D3D12VA, AV_HWDEVICE_TYPE_D3D11VA };
#else
- // NVDEC on NVIDIA GPUs, where it is there; Vulkan Video otherwise,
- // which is how the GPUs of Intel and AMD decode. A machine without
- // the CUDA driver fails to create the first device and goes on to the
- // second.
- return { AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_VULKAN };
+ // NVDEC on NVIDIA GPUs, where it is there; VA-API otherwise, which
+ // is how the GPUs of Intel and AMD decode through the drivers
+ // distributions enable by default; Vulkan Video last. A machine
+ // without the CUDA driver or libva fails to create that device and
+ // goes on to the next. VA-API comes before Vulkan because a device is
+ // picked by whether it can be created, not by whether it decodes: a
+ // Mesa with its video extensions switched off creates a Vulkan device
+ // all the same, and the stream would then go to software without
+ // VA-API being tried.
+ return { AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_VULKAN };
#endif
}
diff --git a/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java b/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java
index ce9d9954f..152681fe0 100644
--- a/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java
+++ b/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java
@@ -107,7 +107,7 @@ public MediaPlayer(MediaReader reader, CustomVideoSource videoSource,
* With {@code hardwareDecoding} the player decodes H.264, H.265/HEVC, VP9
* and AV1 video on the media engine or GPU of the machine where there is one the platform
* offers (VideoToolbox on macOS, Direct3D 12 or 11 on Windows, NVDEC on
- * NVIDIA GPUs and Vulkan Video on others on Linux), which takes less
+ * NVIDIA GPUs and VA-API or Vulkan Video on others on Linux), which takes less
* processor time, by an amount
* that depends on the platform and the stream. The
* pictures are the same as software decoding gives. A stream the
diff --git a/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/HardwareDecodingTest.java b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/HardwareDecodingTest.java
index cc6040f30..b89c3187e 100644
--- a/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/HardwareDecodingTest.java
+++ b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/HardwareDecodingTest.java
@@ -90,7 +90,7 @@ class HardwareDecodingTest {
private static final String VP9 = "/media-test-vp9.webm";
- /** VP9 profile 1, 4:4:4, which no hardware decoder takes. */
+ /** VP9 profile 1, 4:4:4, which Intel's hardware takes through VA-API and no other does. */
private static final String VP9_444 = "/media-test-vp9-444.webm";
/**
@@ -318,14 +318,14 @@ void codecWithoutHardwareFallsBack() throws Exception {
@Test
void streamTheHardwareDoesNotTakeFallsBack() throws Exception {
- Result software = play(VP9_444, false, 0);
- Result hardware = play(VP9_444, true, 0);
+ Result hardware = playNotTakenByHardware();
+ Result software = play(hardware.asset, false, 0);
assertNull(hardware.error);
// Asked for hardware, it plays in software, with the same pictures.
assertFalse(hardware.hardware);
- assertEquals(FRAMES, hardware.frames.size());
+ assertEquals(software.frames.size(), hardware.frames.size());
assertEquals(software.frames, hardware.frames);
}
@@ -458,13 +458,13 @@ void softwareThatTakesOverUsesEveryCore() throws Exception {
requireHardwareContext();
- // VP9 in 4:4:4: the hardware decoder offers no format for it, and the
- // stream is decoded in software. The decoder made for the hardware has
- // one thread, so it is replaced by one that has the cores of the
- // machine.
+ // A stream the hardware decoder offers no format for is decoded in
+ // software. The decoder made for the hardware has one thread, so it is
+ // replaced by one that has the cores of the machine. The stream played
+ // last is the one that counts.
HardwareFault.disarm();
- Result hardware = play(VP9_444, true, 0);
+ Result hardware = playNotTakenByHardware();
assertNull(hardware.error);
assertFalse(hardware.hardware);
@@ -753,22 +753,32 @@ void h264444PlaysAsInSoftware() throws Exception {
assertPlaysAsInSoftware(H264_444);
}
+ @Test
+ void vp9444PlaysAsInSoftware() throws Exception {
+ assertPlaysAsInSoftware(VP9_444, FRAMES);
+ }
+
/**
* A stream that not every hardware decoder takes plays with the pictures
* software decoding gives and no error: in software where the hardware does
* not take it, and in hardware where it does. VideoToolbox decodes High 10
* and 4:4:4 H.264 on macOS 15, and hands the pictures over as P010 and
* NV24, whose luma swscale brings to 8 bits exactly as it does that of the
- * planar formats of software decoding.
+ * planar formats of software decoding. Intel's hardware decodes 4:4:4 VP9
+ * through VA-API.
*/
private void assertPlaysAsInSoftware(String asset) throws Exception {
+ assertPlaysAsInSoftware(asset, 15);
+ }
+
+ private void assertPlaysAsInSoftware(String asset, int frames) throws Exception {
Result software = play(asset, false, 0);
Result hardware = play(asset, true, 0);
assertNull(software.error);
assertNull(hardware.error);
- assertEquals(15, hardware.frames.size());
+ assertEquals(frames, hardware.frames.size());
assertEquals(software.frames, hardware.frames);
}
@@ -915,6 +925,33 @@ private static void requireHardware(boolean hardware, boolean required) {
}
}
+ /**
+ * Streams in formats that some hardware decoders take and others do not:
+ * VP9 in 4:4:4, which Intel's takes through VA-API, and H.264 in 4:4:4 and
+ * High 10, which VideoToolbox takes on macOS 15.
+ */
+ private static final String[] NOT_ALWAYS_TAKEN = { VP9_444, H264_444, H264_10BIT };
+
+ /**
+ * Plays, asking for hardware, the first stream of {@link #NOT_ALWAYS_TAKEN}
+ * that the hardware of this machine does not take, and returns what that
+ * played. Skips the test where the hardware takes them all, as there is no
+ * stream to fall back with.
+ */
+ private Result playNotTakenByHardware() throws Exception {
+ for (String asset : NOT_ALWAYS_TAKEN) {
+ Result result = play(asset, true, 0);
+
+ if (!result.hardware) {
+ return result;
+ }
+ }
+
+ assumeTrue(false, "the hardware takes every stream that some hardware does not");
+
+ return null;
+ }
+
/**
* Plays the asset from the given position to its end, in real time, and
* returns what a sink on the track saw: a checksum of the luma of each
@@ -922,6 +959,7 @@ private static void requireHardware(boolean hardware, boolean required) {
*/
private Result play(String asset, boolean hardware, long positionUs) throws Exception {
Result result = new Result();
+ result.asset = asset;
CountDownLatch ended = new CountDownLatch(1);
CustomVideoSource source = new CustomVideoSource();
@@ -1046,6 +1084,7 @@ private static Path path(String asset) throws Exception {
private static final class Result {
final List frames = Collections.synchronizedList(new ArrayList<>());
+ volatile String asset;
volatile String error;
volatile boolean hardware;
volatile String size;