From efd63c00c1fc919e793c33ca02b14da6e4148f56 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 20 Sep 2026 19:26:34 +0200 Subject: [PATCH 01/12] build: teach the FFmpeg build to cross-compile for every target The media module had no platform profiles beyond Windows, so a Linux or macOS build had no compiler or sysroot for its target and CMake simply built for the host. It now takes a toolchain file the way webrtc-jni does, and its profiles mirror webrtc-jni's by name and by the file each one selects, so both native libraries of a given classifier are built for the same target the same way. Keeping one set of toolchain files rather than a second copy is deliberate: they are where this project says which compiler and which sysroot a target uses, and two answers would drift. What configure is told about the target is worked out from what the toolchain file already set -- the architecture from CMAKE_SYSTEM_PROCESSOR, the target OS from the platform, the compiler from CMAKE_C_COMPILER, the target flags from CMAKE_C_FLAGS, the sysroot from CMAKE_SYSROOT -- so a target CMake can already build for needs no new case. Apple is the exception it has to make: there the architecture is a compiler flag rather than a different compiler. Cross-compiled arm and aarch64 start with --disable-asm. An assembler for a foreign architecture is a build prerequisite of its own, and nothing here is fast enough yet to need one; correctness on every platform first. Only windows-x86_64 is verified, because it is the only target this machine can build. It still builds from nothing and its configure line is unchanged apart from an explicit --arch=x86_64, and the module's 19 tests pass. Everything else waits on CI. --- webrtc-java-media/pom.xml | 68 ++++++++++++++ webrtc-java-media/src/main/cpp/CMakeLists.txt | 21 +++++ .../cpp/dependencies/ffmpeg/CMakeLists.txt | 90 +++++++++++++++++-- 3 files changed, 170 insertions(+), 9 deletions(-) diff --git a/webrtc-java-media/pom.xml b/webrtc-java-media/pom.xml index 45c08edf..18906264 100644 --- a/webrtc-java-media/pom.xml +++ b/webrtc-java-media/pom.xml @@ -29,6 +29,12 @@ ABI, so it needs no special toolchain or standard library. --> + + + ${project.basedir}/../webrtc-jni/src/main/cpp/toolchain @@ -73,6 +79,9 @@ + @@ -149,6 +158,64 @@ + + + linux-x86_64 + + + linux + amd64 + + + + ${media.toolchain.dir}/x86_64-linux-clang.cmake + + + + linux-aarch32 + + + linux + aarch32 + + + + ${media.toolchain.dir}/aarch32-linux-clang.cmake + + + + linux-aarch64 + + + linux + aarch64 + + + + ${media.toolchain.dir}/aarch64-linux-clang.cmake + + + + macos-aarch64 + + + mac + aarch64 + + + + ${media.toolchain.dir}/aarch64-macos-clang.cmake + + + + macos-cross-x86_64 + + ${media.toolchain.dir}/x86_64-macos-cross.cmake + + + @@ -190,6 +257,7 @@ -AARM64 ${cmake.build.type} + ${media.toolchain.dir}/aarch64-windows-clang.cmake diff --git a/webrtc-java-media/src/main/cpp/CMakeLists.txt b/webrtc-java-media/src/main/cpp/CMakeLists.txt index c9a8c294..bd3268e8 100644 --- a/webrtc-java-media/src/main/cpp/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/CMakeLists.txt @@ -1,6 +1,27 @@ cmake_minimum_required(VERSION 3.13) + +# +# A cross build is driven by a toolchain file, the same ones webrtc-jni uses: +# they are where this project says which compiler and which sysroot a target is +# built with, and having two answers to that would only let them drift apart. +# It has to be set before project(), which is why it arrives under a name of +# its own rather than as CMAKE_TOOLCHAIN_FILE. +# +if(DEFINED MEDIA_TOOLCHAIN_FILE AND NOT MEDIA_TOOLCHAIN_FILE STREQUAL "") + message(STATUS "Toolchain file: ${MEDIA_TOOLCHAIN_FILE}") + set(CMAKE_TOOLCHAIN_FILE "${MEDIA_TOOLCHAIN_FILE}") +endif() + project(webrtc-java-media CXX) +# The toolchain files find a sysroot but leave it to be applied here, after +# project(), because setting it earlier breaks CMake's own compiler checks. +if(DEFINED DEFERRED_SYSROOT AND DEFERRED_SYSROOT) + message(STATUS "Sysroot: ${DEFERRED_SYSROOT}") + set(CMAKE_SYSROOT ${DEFERRED_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${DEFERRED_SYSROOT}) +endif() + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt index 30fe26a8..0ede65cb 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -229,23 +229,95 @@ else() --enable-protocol=pipe ) + # + # What configure has to be told about the target. It is all worked out from + # what the toolchain file already set, so a target CMake can build for + # needs no new case here. + # + + # The architecture under the name FFmpeg knows it by. + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + set(FFMPEG_ARCH x86_64) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") + set(FFMPEG_ARCH aarch64) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm|armv7.*|armhf)$") + set(FFMPEG_ARCH arm) + else() + set(FFMPEG_ARCH "") + endif() + if(WIN32) - # FFmpeg builds against the MSVC runtime through its own msvc - # toolchain support, which keeps the DLLs free of an MSYS2 runtime - # dependency. configure needs Windows-style output, so cl and link - # must be on the PATH of the shell that runs it. + set(FFMPEG_TARGET_OS win64) + + # FFmpeg builds against the MSVC runtime through its own msvc toolchain + # support, which keeps the DLLs free of an MSYS2 runtime dependency. + # configure needs Windows-style output, so cl and link must be on the + # PATH of the shell that runs it. list(APPEND FFMPEG_CONFIGURE_ARGS --toolchain=msvc) + elseif(APPLE) + set(FFMPEG_TARGET_OS darwin) + else() + set(FFMPEG_TARGET_OS linux) endif() - if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$" AND WIN32) - # No assembler for this target yet; correctness first, speed later. - list(APPEND FFMPEG_CONFIGURE_ARGS --disable-asm --arch=arm64) + if(FFMPEG_ARCH) + list(APPEND FFMPEG_CONFIGURE_ARGS --arch=${FFMPEG_ARCH}) endif() + set(FFMPEG_EXTRA_CFLAGS "") + set(FFMPEG_EXTRA_LDFLAGS "") + if(APPLE AND CMAKE_OSX_ARCHITECTURES) + # On Apple the architecture is chosen by a compiler flag rather than by + # a different compiler, and the deployment target has to match what the + # rest of the build asks for or the linker complains about every object. + set(FFMPEG_EXTRA_CFLAGS "-arch ${CMAKE_OSX_ARCHITECTURES}") + set(FFMPEG_EXTRA_LDFLAGS "-arch ${CMAKE_OSX_ARCHITECTURES}") + + if(CMAKE_OSX_DEPLOYMENT_TARGET) + set(FFMPEG_EXTRA_CFLAGS + "${FFMPEG_EXTRA_CFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + set(FFMPEG_EXTRA_LDFLAGS + "${FFMPEG_EXTRA_LDFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + elseif(NOT WIN32) + # Elsewhere the target is a compiler flag the toolchain file put into + # CMAKE_C_FLAGS, which is exactly what FFmpeg needs to be handed too. + set(FFMPEG_EXTRA_CFLAGS "${CMAKE_C_FLAGS}") + set(FFMPEG_EXTRA_LDFLAGS "${CMAKE_C_FLAGS}") + endif() + + if(CMAKE_SYSROOT) + list(APPEND FFMPEG_CONFIGURE_ARGS --sysroot=${CMAKE_SYSROOT}) + endif() + + if(CMAKE_CROSSCOMPILING) list(APPEND FFMPEG_CONFIGURE_ARGS - --extra-cflags=-arch\ ${CMAKE_OSX_ARCHITECTURES} - --extra-ldflags=-arch\ ${CMAKE_OSX_ARCHITECTURES}) + --enable-cross-compile + --target-os=${FFMPEG_TARGET_OS}) + + # An assembler for a foreign architecture is a build prerequisite of + # its own, and this module is not fast enough to need one yet. + # Correctness on every platform first, speed where it is measured. + if(FFMPEG_ARCH STREQUAL "aarch64" OR FFMPEG_ARCH STREQUAL "arm") + list(APPEND FFMPEG_CONFIGURE_ARGS --disable-asm) + endif() + endif() + + # The compiler CMake settled on, so that FFmpeg is built by the same one as + # the rest of this module rather than by whatever configure finds first. + if(NOT WIN32 AND CMAKE_C_COMPILER) + list(APPEND FFMPEG_CONFIGURE_ARGS --cc=${CMAKE_C_COMPILER}) + endif() + + # These carry spaces, so they are quoted here rather than in the script. + if(NOT FFMPEG_EXTRA_CFLAGS STREQUAL "") + string(STRIP "${FFMPEG_EXTRA_CFLAGS}" FFMPEG_EXTRA_CFLAGS) + list(APPEND FFMPEG_CONFIGURE_ARGS "--extra-cflags=\"${FFMPEG_EXTRA_CFLAGS}\"") + endif() + if(NOT FFMPEG_EXTRA_LDFLAGS STREQUAL "") + string(STRIP "${FFMPEG_EXTRA_LDFLAGS}" FFMPEG_EXTRA_LDFLAGS) + list(APPEND FFMPEG_CONFIGURE_ARGS "--extra-ldflags=\"${FFMPEG_EXTRA_LDFLAGS}\"") endif() # Build out of tree so the submodule working tree stays clean. From 3c267a3f90fea8f095723873126d8d84bf41317f Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 20 Sep 2026 19:26:43 +0200 Subject: [PATCH 02/12] ci: build FFmpeg on every platform and cache it The media module is about to become part of the normal build, so every native build job has to be able to produce its FFmpeg, and none of them could. Windows gets MSYS2 with make and nasm, because FFmpeg's configure and makefiles are shell scripts and building it needs a POSIX shell even with MSVC. MSYS2_ROOT is exported so the media module's CMake finds it wherever the runner image put it. Linux and macOS get nasm, which is what FFmpeg assembles its x86 code with; macOS needs it even though the runner is Apple Silicon, because the Intel build is cross-compiled on it. The FFmpeg install directory is cached the same way the WebRTC checkout already is, keyed by FFmpeg version, platform and the hash of the dependency script and the module pom, so only the first build of a platform pays for it. The three native build jobs in each workflow now check out submodules, since the FFmpeg source is one. test-natives does not, because it runs against downloaded natives and never builds the module. --- .github/actions/build/action.yml | 11 +++++++++++ .github/actions/prepare-linux/action.yml | 3 ++- .github/actions/prepare-macos/action.yml | 3 +++ .github/actions/prepare-windows/action.yml | 13 +++++++++++++ .github/actions/release/action.yml | 11 +++++++++++ .github/workflows/build.yml | 9 +++++++++ .github/workflows/release.yml | 6 ++++++ 7 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index a9f167c5..99ca8e90 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -34,6 +34,17 @@ runs: key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} restore-keys: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}- + - name: Derive the FFmpeg version from the pom + run: echo "FFMPEG_VERSION=$(sed -n 's|.*\([^<]*\).*|\1|p' webrtc-java-media/pom.xml)" >> "$GITHUB_ENV" + shell: bash + + - name: Set up FFmpeg cache + uses: actions/cache@v4 + with: + path: ~/ffmpeg + key: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}-${{ hashFiles('webrtc-java-media/pom.xml') }} + restore-keys: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}- + - name: Set up Maven cache uses: actions/cache@v4 with: diff --git a/.github/actions/prepare-linux/action.yml b/.github/actions/prepare-linux/action.yml index 6fbf9f30..23f07c40 100644 --- a/.github/actions/prepare-linux/action.yml +++ b/.github/actions/prepare-linux/action.yml @@ -31,7 +31,8 @@ runs: - name: Install required packages run: | sudo apt update - sudo apt install -y binutils cmake git locales lsb-release ninja-build pipewire pipewire-pulse pkg-config python3 python3-setuptools rsync unzip wget xz-utils + # nasm is what FFmpeg assembles its x86 code with. + sudo apt install -y binutils cmake git locales lsb-release nasm ninja-build pipewire pipewire-pulse pkg-config python3 python3-setuptools rsync unzip wget xz-utils # Chromium Clang to be used with the clang toolchain file #curl -s https://raw.githubusercontent.com/chromium/chromium/main/tools/clang/scripts/update.py | python3 - --output-dir=/opt/clang diff --git a/.github/actions/prepare-macos/action.yml b/.github/actions/prepare-macos/action.yml index 818e04ea..5ef69300 100644 --- a/.github/actions/prepare-macos/action.yml +++ b/.github/actions/prepare-macos/action.yml @@ -9,6 +9,9 @@ runs: run: | # Required on macos-14 brew install python-setuptools + # nasm is what FFmpeg assembles its x86 code with, which the Intel + # build needs even though the runner itself is Apple Silicon. + brew install nasm shell: bash - name: Select Xcode version diff --git a/.github/actions/prepare-windows/action.yml b/.github/actions/prepare-windows/action.yml index ff9b023a..dd0e96a5 100644 --- a/.github/actions/prepare-windows/action.yml +++ b/.github/actions/prepare-windows/action.yml @@ -38,3 +38,16 @@ runs: run: | choco install ninja shell: powershell + + - name: Install MSYS2 and the FFmpeg build tools + run: | + # FFmpeg's configure and makefiles are shell scripts, so building it + # needs a POSIX shell even with MSVC, and that shell has to carry make + # and nasm. Git for Windows' bash carries neither. + if (-not (Test-Path "C:\msys64\usr\bin\bash.exe")) { + choco install msys2 --no-progress -y + } + C:\msys64\usr\bin\bash.exe -lc "pacman -Sy --noconfirm && pacman -S --needed --noconfirm make nasm diffutils pkgconf" + # Where the media module's CMake looks for it. + "MSYS2_ROOT=C:\msys64" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + shell: powershell diff --git a/.github/actions/release/action.yml b/.github/actions/release/action.yml index fa20ed67..0c6fe4af 100644 --- a/.github/actions/release/action.yml +++ b/.github/actions/release/action.yml @@ -25,6 +25,17 @@ runs: key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} restore-keys: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}- + - name: Derive the FFmpeg version from the pom + run: echo "FFMPEG_VERSION=$(sed -n 's|.*\([^<]*\).*|\1|p' webrtc-java-media/pom.xml)" >> "$GITHUB_ENV" + shell: bash + + - name: Set up FFmpeg cache + uses: actions/cache@v4 + with: + path: ~/ffmpeg + key: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}-${{ hashFiles('webrtc-java-media/pom.xml') }} + restore-keys: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}- + - name: Set up Maven cache uses: actions/cache@v4 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c63b2fe..386087dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + # The media module builds FFmpeg from its third-party submodule. + submodules: true - id: prepare name: Prepare build @@ -72,6 +75,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + # The media module builds FFmpeg from its third-party submodule. + submodules: true - id: prepare name: Prepare build @@ -108,6 +114,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + # The media module builds FFmpeg from its third-party submodule. + submodules: true - id: prepare-build name: Prepare build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0684e52f..33085969 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,6 +148,8 @@ jobs: uses: actions/checkout@v6 with: fetch-tags: true + # The media module builds FFmpeg from its third-party submodule. + submodules: true - name: Get tag from current branch id: tag-selector @@ -189,6 +191,8 @@ jobs: uses: actions/checkout@v6 with: fetch-tags: true + # The media module builds FFmpeg from its third-party submodule. + submodules: true - name: Get tag from current branch id: tag-selector @@ -227,6 +231,8 @@ jobs: uses: actions/checkout@v6 with: fetch-tags: true + # The media module builds FFmpeg from its third-party submodule. + submodules: true - name: Get tag from current branch id: tag-selector From 4399bb250cd467266b84a2746e1cdcfa291beb47 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 20 Sep 2026 19:26:43 +0200 Subject: [PATCH 03/12] build: make the media module part of the normal build The module was opt-in behind a profile while it was being brought up. It now builds with everything else, which is also what lets CI prove it on the platforms this machine cannot build for. That collapses the awkward part of the examples module. The media example needed a source directory, a compile execution and an output directory of its own, purely because a JPMS "requires" cannot be made conditional and the module it names has to exist. With the module always there, the example moves in beside the others and the whole profile becomes one dependency and one "requires", exactly as its comment said it would. Anything building this project now needs the third-party/ffmpeg submodule checked out, and make and nasm to build it. The README and the guide say so where they used to describe the profile. --- docs/guide/examples.md | 6 +- docs/guide/media/media-files.md | 10 +-- pom.xml | 14 +--- webrtc-examples/pom.xml | 76 ++----------------- .../webrtc/examples/MediaFileExample.java | 0 .../src/main/java/module-info.java | 1 + webrtc-java-media/README.md | 5 +- 7 files changed, 17 insertions(+), 95 deletions(-) rename webrtc-examples/src/{media => main}/java/dev/onvoid/webrtc/examples/MediaFileExample.java (100%) diff --git a/docs/guide/examples.md b/docs/guide/examples.md index ae9191cb..a39d646f 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -65,11 +65,11 @@ The [`MediaFileExample`](https://github.com/devopvoid/webrtc-java/blob/master/we This example is useful for applications that stream pre-recorded media, or that need a dependable stand-in for a camera in testing. -::: warning -This example needs the `webrtc-java-media` module, which is opt-in while it is being brought up on every platform, so it is built and run with the `with-media-extension` profile: +::: info +This example needs the `webrtc-java-media` module, which builds FFmpeg from the `third-party/ffmpeg` submodule. Make sure the submodule is checked out before building, as the [Media Files](/guide/media/media-files) guide describes. ```bash -mvn -Pwith-media-extension exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.MediaFileExample" -D"exec.args=movie.mp4" +mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.MediaFileExample" -D"exec.args=movie.mp4" ``` ::: diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index 56874a1e..51ddad9d 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -12,17 +12,13 @@ Sending a file is a common need: a test pattern instead of a webcam, a pre-recor Decoding happens entirely in native code. Frames never travel through Java: the module hands decoded pictures straight to the native side of a `CustomVideoSource`, and 10 ms chunks to a `CustomAudioSource`. They are paced in real time and carry the presentation times of the file, so what a receiver gets keeps the timing of the media rather than the timing of a Java thread. -::: warning Opt-in while it is being brought up -The media module is not part of the default build yet, and its native library has so far been built for `windows-x86_64` only. Build it with the `with-media-extension` profile, as described below. -::: - ## Adding the Module -The module builds FFmpeg from a submodule pinned to a release tag, so the submodule has to be present: +The module is part of the normal build, and it builds FFmpeg from a submodule pinned to a release tag, so the submodule has to be present: ```shell git submodule update --init --depth 1 webrtc-java-media/third-party/ffmpeg -mvn install -Pwith-media-extension +mvn install ``` Building FFmpeg needs `make` and `nasm`. On Windows they come from MSYS2: @@ -228,5 +224,5 @@ The module uses FFmpeg under the LGPL version 2.1 or later. It is configured wit See `MediaFileExample` in the `webrtc-examples` module, which opens a file, reports what it contains, creates tracks, adds them to a peer connection and follows playback to the end. ```shell -mvn -Pwith-media-extension -pl webrtc-examples compile +mvn -pl webrtc-examples compile ``` diff --git a/pom.xml b/pom.xml index b8e6770a..90d3a69d 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ webrtc-jni webrtc + webrtc-java-media webrtc-examples @@ -354,19 +355,6 @@ webrtc.macos.aarch64 - - - with-media-extension - - webrtc-java-media - - diff --git a/webrtc-examples/pom.xml b/webrtc-examples/pom.xml index cf9437d3..d24312c7 100644 --- a/webrtc-examples/pom.xml +++ b/webrtc-examples/pom.xml @@ -65,6 +65,12 @@ ${project.version} + + ${project.groupId} + webrtc-java-media + ${project.version} + + org.eclipse.jetty jetty-server @@ -95,74 +101,4 @@ - - - - with-media-extension - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - compile-media-example - compile - - compile - - - 17 - - ${project.basedir}/src/media/java - - ${project.build.directory}/media-classes - - - - - - - - org.codehaus.mojo - exec-maven-plugin - 3.5.0 - - - ${project.build.directory}/media-classes - - - - - - - - - ${project.groupId} - webrtc-java-media - ${project.version} - - - - \ No newline at end of file diff --git a/webrtc-examples/src/media/java/dev/onvoid/webrtc/examples/MediaFileExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java similarity index 100% rename from webrtc-examples/src/media/java/dev/onvoid/webrtc/examples/MediaFileExample.java rename to webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java diff --git a/webrtc-examples/src/main/java/module-info.java b/webrtc-examples/src/main/java/module-info.java index b330984a..28989e1f 100644 --- a/webrtc-examples/src/main/java/module-info.java +++ b/webrtc-examples/src/main/java/module-info.java @@ -6,6 +6,7 @@ requires org.eclipse.jetty.server; requires org.eclipse.jetty.websocket.server; requires webrtc.java; + requires webrtc.java.media; exports dev.onvoid.webrtc.examples.web.client; exports dev.onvoid.webrtc.examples.web.server; diff --git a/webrtc-java-media/README.md b/webrtc-java-media/README.md index ef048c1c..47bbaff5 100644 --- a/webrtc-java-media/README.md +++ b/webrtc-java-media/README.md @@ -39,11 +39,12 @@ licensed under the Apache License 2.0 like the rest of webrtc-java. ## Building -The submodule has to be present: +This module is part of the normal build, and it builds FFmpeg from a submodule, so the submodule +has to be present: ```shell git submodule update --init --depth 1 webrtc-java-media/third-party/ffmpeg -mvn install -Pwith-media-extension +mvn install ``` The first build compiles FFmpeg, which takes a while; later builds reuse the install directory From 9a9bba6479e3153fa800f7dbc5a845cb18e4eaf9 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 21 Sep 2026 08:13:42 +0200 Subject: [PATCH 04/12] fix: stop the media module depending on natives it builds itself Every CI job failed at dependency resolution before a single mojo ran: Could not find artifact dev.onvoid.webrtc:webrtc-java-media:jar:windows-x86_64:0.19.0-SNAPSHOT The module declared a dependency on its own classifier artifact so that anything using it would get the natives along the way. That cannot work on a clean machine: the artifact is built and attached by this same module, so at the moment its dependencies are resolved nothing has produced it yet. It only passed locally because an earlier install had left the artifact in the local repository. webrtc-java can do this because webrtc-jni, a module earlier in the reactor, is what builds its natives, and because the artifact has been published besides. Neither is true here. So the dependency is gone, and anything using the module asks for the natives itself with a second dependency carrying the platform classifier. webrtc-examples does, and the guide says so. The module's own tests never needed it: Surefire is given the directory the CMake build collects the natives into. Verified by emptying dev/onvoid/webrtc/webrtc-java-media out of the local repository first, which reproduced the CI failure exactly, and then building clean: 168 tests in webrtc and 19 in webrtc-java-media, all five modules green. --- docs/guide/media/media-files.md | 14 ++++++++++++-- webrtc-examples/pom.xml | 9 +++++++++ webrtc-java-media/pom.xml | 22 ++++++++++++---------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index 51ddad9d..e727cde1 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -30,7 +30,7 @@ C:\msys64\usr\bin\bash -lc "pacman -S --needed make nasm diffutils pkgconf" Maven still runs from an ordinary shell; the build enters MSYS2 and the Visual Studio environment on its own. The first build compiles FFmpeg, which takes a while; later builds reuse the install directory. -Once installed, depend on it alongside `webrtc-java`: +Once installed, depend on it alongside `webrtc-java`. It takes two entries: one for the Java API, and one for the natives of the platform you are running on. ```xml @@ -38,9 +38,19 @@ Once installed, depend on it alongside `webrtc-java`: webrtc-java-media 0.19.0-SNAPSHOT + + dev.onvoid.webrtc + webrtc-java-media + 0.19.0-SNAPSHOT + windows-x86_64 + ``` -The classifier jar carries the module's native library together with the FFmpeg shared libraries it uses. Applications that do not use this module never download FFmpeg. +The classifier jar carries the module's native library together with the FFmpeg shared libraries it uses, so applications that do not use this module never download FFmpeg. Replace the classifier with the platform you are building for: `windows-x86_64`, `windows-aarch64`, `linux-x86_64`, `linux-aarch64`, `linux-aarch32`, `macos-x86_64` or `macos-aarch64`. + +::: info +Unlike `webrtc-java`, which brings its natives along by itself, this module cannot: the natives are built by the module rather than by a separate one, so a dependency on them would have nothing to resolve against on a first build. Asking for them explicitly is the price of that. +::: ## Sending a File diff --git a/webrtc-examples/pom.xml b/webrtc-examples/pom.xml index d24312c7..485eaea2 100644 --- a/webrtc-examples/pom.xml +++ b/webrtc-examples/pom.xml @@ -71,6 +71,15 @@ ${project.version} + + + ${project.groupId} + webrtc-java-media + ${project.version} + ${platform.classifier} + + org.eclipse.jetty jetty-server diff --git a/webrtc-java-media/pom.xml b/webrtc-java-media/pom.xml index 18906264..75de5b81 100644 --- a/webrtc-java-media/pom.xml +++ b/webrtc-java-media/pom.xml @@ -139,22 +139,24 @@ + dev.onvoid.webrtc webrtc-java ${project.version} - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${platform.classifier} - From ed41b13fd7542917a39db80023d221a1c68b9af0 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 21 Sep 2026 08:34:32 +0200 Subject: [PATCH 05/12] fix: normalise the FFmpeg install path before CMake parses it Every job failed configuring the media module: CMake Error at dependencies/ffmpeg/CMakeLists.txt:162 (set): when parsing string C:\Users\runneradmin/ffmpeg/windows-x86_64 Invalid character escape '\U' Two things had to line up for this, which is why it never showed up locally. Maven hands the install directory over as ${user.home} plus a suffix, and on Windows that is a backslash path, which CMake reads as the start of an escape sequence. And to_shell_path was a macro, so its argument was substituted into the body as text rather than passed as a value, leaving CMake to parse C:\Users\... as code. It stayed hidden because that code only runs when FFmpeg has to be built. This machine already had it installed, so every local build took the short-circuit and never reached the macro. CI, starting from nothing, reached it immediately. The path is now normalised with file(TO_CMAKE_PATH) as it arrives, and to_shell_path is a function, which passes its arguments as values. Either change alone would have been enough; both are worth having, since the first fixes this input and the second fixes the class of it. Verified the way it should have been the first time: by deleting the FFmpeg install directory so that the build path is the one that runs. The failure reproduces exactly beforehand, and afterwards "mvn clean package -DskipTests" builds FFmpeg from nothing and every module succeeds, with the module's 19 tests green. --- .../cpp/dependencies/ffmpeg/CMakeLists.txt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt index 0ede65cb..18650aed 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -25,6 +25,11 @@ if(NOT DEFINED FFMPEG_INSTALL_DIR OR FFMPEG_INSTALL_DIR STREQUAL "") message(FATAL_ERROR "FFMPEG_INSTALL_DIR is not set") endif() +# It arrives from Maven, where the user's home on Windows is a backslash +# path. CMake reads a backslash as the start of an escape, so C:\Users\... +# is not only the wrong separator, it is a parse error waiting to happen. +file(TO_CMAKE_PATH "${FFMPEG_INSTALL_DIR}" FFMPEG_INSTALL_DIR) + file(MAKE_DIRECTORY "${FFMPEG_INSTALL_DIR}") # The libraries this module needs, in dependency order. The order matters @@ -158,13 +163,18 @@ else() # configure and make are run by a POSIX shell, which on Windows means MSYS2 # and its /c/... form of a path rather than C:/... - macro(to_shell_path PATH_IN RESULT) - set(${RESULT} "${PATH_IN}") + # A function rather than a macro on purpose: a macro substitutes its + # arguments into the body as text, so a path still holding a backslash + # would be parsed as an escape sequence instead of passed as a value. + function(to_shell_path PATH_IN RESULT) + set(CONVERTED "${PATH_IN}") if(WIN32) - string(REGEX REPLACE "^([A-Za-z]):/" "/\\1/" ${RESULT} "${${RESULT}}") + string(REGEX REPLACE "^([A-Za-z]):/" "/\\1/" CONVERTED "${CONVERTED}") endif() - endmacro() + + set(${RESULT} "${CONVERTED}" PARENT_SCOPE) + endfunction() to_shell_path("${FFMPEG_INSTALL_DIR}" FFMPEG_INSTALL_DIR_SH) to_shell_path("${FFMPEG_SOURCE_DIR}" FFMPEG_SOURCE_DIR_SH) From cedce083af0e4d7e28abedc9bade1b6c163062f2 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 21 Sep 2026 19:56:32 +0200 Subject: [PATCH 06/12] fix: give each target a toolchain the media module can actually build with The first CI run that reached FFmpeg turned up three separate faults, all of them from reusing webrtc-jni's toolchain files. Those files are not neutral descriptions of a target: they encode how webrtc-jni links WebRTC's C++ ABI, and this module links no C++ ABI at all. On Linux they compile with -nostdinc++, because webrtc-jni supplies WebRTC's own libc++ through include paths of its own. This module inherited the flag and none of the paths, so its first #include failed. It now uses toolchain files of its own that name the distribution's cross compilers and nothing else. linux-x86_64 needs no file at all, being built natively on the runner. Cross builds also tripped over stripping. make install strips what it installs with whichever strip is on the PATH, and the host's cannot read a binary for another architecture: "unable to recognise the format of the input file". FFmpeg is configured with --disable-stripping when cross-compiling, and the toolchain files name the cross binutils. On macOS the toolchain file set CMAKE_SYSTEM_NAME, which makes CMake report a cross build even when host and target agree, so configure was handed --enable-cross-compile for a native build along with --arch=aarch64. Apple's clang knows that architecture as arm64 and rejects the other outright, which configure reports only as being unable to create an executable. Neither macOS target is a cross build -- the Intel one runs under "arch -x86_64" -- so neither takes a file now, and the architecture is spelled the way Apple spells it. windows-aarch64 passed and keeps webrtc-jni's file, which suits it: there the C++ standard library comes from MSVC either way. Verified as far as this machine can: windows-x86_64 unaffected, with the full reactor green. The other targets wait on CI again. --- webrtc-java-media/pom.xml | 33 +++++++++---------- .../cpp/dependencies/ffmpeg/CMakeLists.txt | 15 ++++++++- .../main/cpp/toolchain/aarch32-linux.cmake | 23 +++++++++++++ .../main/cpp/toolchain/aarch64-linux.cmake | 23 +++++++++++++ 4 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 webrtc-java-media/src/main/cpp/toolchain/aarch32-linux.cmake create mode 100644 webrtc-java-media/src/main/cpp/toolchain/aarch64-linux.cmake diff --git a/webrtc-java-media/pom.xml b/webrtc-java-media/pom.xml index 75de5b81..7e0c44e6 100644 --- a/webrtc-java-media/pom.xml +++ b/webrtc-java-media/pom.xml @@ -29,12 +29,15 @@ ABI, so it needs no special toolchain or standard library. --> - + - ${project.basedir}/../webrtc-jni/src/main/cpp/toolchain + ${project.basedir}/src/main/cpp/toolchain + ${project.basedir}/../webrtc-jni/src/main/cpp/toolchain @@ -171,9 +174,8 @@ amd64 - - ${media.toolchain.dir}/x86_64-linux-clang.cmake - + linux-aarch32 @@ -184,7 +186,7 @@ - ${media.toolchain.dir}/aarch32-linux-clang.cmake + ${media.toolchain.dir}/aarch32-linux.cmake @@ -196,7 +198,7 @@ - ${media.toolchain.dir}/aarch64-linux-clang.cmake + ${media.toolchain.dir}/aarch64-linux.cmake @@ -207,15 +209,12 @@ aarch64 - - ${media.toolchain.dir}/aarch64-macos-clang.cmake - + macos-cross-x86_64 - - ${media.toolchain.dir}/x86_64-macos-cross.cmake - + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + + ${project.basedir}/../webrtc-java-media/target/natives + + + + org.apache.maven.plugins maven-compiler-plugin @@ -71,15 +92,6 @@ ${project.version} - - - ${project.groupId} - webrtc-java-media - ${project.version} - ${platform.classifier} - - org.eclipse.jetty jetty-server From 7802c18c8bb440f28ca6c67918df65f7181a0b9b Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 21 Sep 2026 23:18:45 +0200 Subject: [PATCH 08/12] fix: let FFmpeg find the macOS SDK, and print config.log when it cannot Both macOS jobs still stopped at the same place: cc is unable to create an executable file. C compiler test failed. The configure line was right by then -- --arch=arm64, no spurious cross-compile -- and the only unusual thing left on it was --cc, naming the compiler inside the Xcode toolchain: --cc=/Applications/Xcode_16.1.app/.../XcodeDefault.xctoolchain/usr/bin/cc Calling that binary directly skips the /usr/bin/cc shim, and the shim is what runs xcrun to point SDKROOT at an SDK. Without one there is no libSystem to link against, so the link test fails and configure reports it as being unable to create an executable. So --cc is no longer passed on Apple: configure's own default is plain "cc", which goes through the shim. Linux still gets it, since there the compiler is chosen deliberately and there is no shim to lose. That diagnosis came from reading a configure line and reasoning about it, which is a poor way to fix a build. configure writes the real reason into ffbuild/config.log and says almost nothing on the console, and on a build machine nobody can reach that is the same as writing it nowhere. The tail of that file is now printed when the build fails, so the next one of these is read rather than guessed at. Windows and Linux are unaffected: the argument was already withheld on Windows, and the rest only runs after a failure. Verified by rebuilding windows-x86_64 from nothing. --- .../cpp/dependencies/ffmpeg/CMakeLists.txt | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt index 7189f962..5fbc97c7 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -329,7 +329,13 @@ else() # The compiler CMake settled on, so that FFmpeg is built by the same one as # the rest of this module rather than by whatever configure finds first. - if(NOT WIN32 AND CMAKE_C_COMPILER) + # + # Not on Apple. There CMake names the compiler inside the toolchain, and + # calling that directly skips the /usr/bin/cc shim that runs xcrun to + # point SDKROOT at an SDK. Without one there is no libSystem to link + # against, which configure reports only as being unable to create an + # executable. Its own default, plain "cc", goes through the shim. + if(NOT WIN32 AND NOT APPLE AND CMAKE_C_COMPILER) list(APPEND FFMPEG_CONFIGURE_ARGS --cc=${CMAKE_C_COMPILER}) endif() @@ -424,10 +430,29 @@ set MSYS2_PATH_TYPE=inherit ) if(NOT BUILD_RESULT EQUAL 0) + # configure says almost nothing on the console and writes the actual + # reason into config.log, which on a build machine nobody can reach is + # the same as writing it nowhere. The tail of it goes to the console. + set(FFMPEG_CONFIG_LOG "${FFMPEG_BUILD_DIR}/ffbuild/config.log") + + if(EXISTS "${FFMPEG_CONFIG_LOG}") + file(READ "${FFMPEG_CONFIG_LOG}" FFMPEG_CONFIG_LOG_TEXT) + string(LENGTH "${FFMPEG_CONFIG_LOG_TEXT}" FFMPEG_CONFIG_LOG_LENGTH) + + if(FFMPEG_CONFIG_LOG_LENGTH GREATER 6000) + math(EXPR FFMPEG_CONFIG_LOG_OFFSET "${FFMPEG_CONFIG_LOG_LENGTH} - 6000") + string(SUBSTRING "${FFMPEG_CONFIG_LOG_TEXT}" + ${FFMPEG_CONFIG_LOG_OFFSET} -1 FFMPEG_CONFIG_LOG_TEXT) + endif() + + message("---- tail of ${FFMPEG_CONFIG_LOG} ----") + message("${FFMPEG_CONFIG_LOG_TEXT}") + message("---- end of config.log ----") + endif() + message(FATAL_ERROR "Building FFmpeg failed (${BUILD_RESULT}). The script that ran is " - "${FFMPEG_BUILD_SCRIPT}; see ${FFMPEG_BUILD_DIR}/ffbuild/config.log for what " - "configure could not find.") + "${FFMPEG_BUILD_SCRIPT}.") endif() ffmpeg_is_installed(FFMPEG_INSTALLED) From 38de02920eb1df047aee150236ab52f666019fc8 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 21 Sep 2026 23:56:36 +0200 Subject: [PATCH 09/12] build: build the Linux natives against the Chromium sysroot The media natives were built with the distribution's compilers and no sysroot, so they needed a newer glibc than webrtc-java's do. A machine could have run one and not the other, which is not a sensible thing to ship in the same release. They now build through webrtc-jni's toolchain files, which is what the plan said in the first place, and so against the same sysroot. That also means -nostdinc++, since everything Linux in this project uses the libc++ WebRTC bundles rather than whatever the build machine happens to have. Not having understood that is what sent this down the distribution toolchain road: the flag arrived without the include paths that go with it and the first #include failed, which looked like a reason to abandon those files rather than to finish using them. So this module now takes libc++ from webrtc.install.dir, and webrtc-jni has to be built before it on Linux. That is the only thing it takes from that build, and it is a C++ standard library, not any part of WebRTC: no WebRTC symbol is linked here, and the two native libraries still meet only through the C function table. The build says so plainly if the headers are not there. While in here, cross-compilation is decided by comparing host and target rather than by CMAKE_CROSSCOMPILING, which every toolchain file in this project sets simply by naming a system. Believing it is what handed configure --enable-cross-compile for a native macOS build earlier, and it would have done the same for linux-x86_64. Verified on windows-x86_64, which is unaffected and still builds FFmpeg from nothing with 19 tests green. Linux waits on CI. --- webrtc-java-media/pom.xml | 30 +++++++---- webrtc-java-media/src/main/cpp/CMakeLists.txt | 33 ++++++++++++ .../cpp/dependencies/ffmpeg/CMakeLists.txt | 50 +++++++++++++------ .../main/cpp/toolchain/aarch32-linux.cmake | 23 --------- .../main/cpp/toolchain/aarch64-linux.cmake | 23 --------- 5 files changed, 87 insertions(+), 72 deletions(-) delete mode 100644 webrtc-java-media/src/main/cpp/toolchain/aarch32-linux.cmake delete mode 100644 webrtc-java-media/src/main/cpp/toolchain/aarch64-linux.cmake diff --git a/webrtc-java-media/pom.xml b/webrtc-java-media/pom.xml index 7e0c44e6..dd1c178f 100644 --- a/webrtc-java-media/pom.xml +++ b/webrtc-java-media/pom.xml @@ -29,15 +29,19 @@ ABI, so it needs no special toolchain or standard library. --> - + - ${project.basedir}/src/main/cpp/toolchain ${project.basedir}/../webrtc-jni/src/main/cpp/toolchain + + ${user.home}/webrtc/build @@ -85,6 +89,9 @@ + @@ -174,8 +181,9 @@ amd64 - + + ${jni.toolchain.dir}/x86_64-linux-clang.cmake + linux-aarch32 @@ -186,7 +194,7 @@ - ${media.toolchain.dir}/aarch32-linux.cmake + ${jni.toolchain.dir}/aarch32-linux-clang.cmake @@ -198,7 +206,7 @@ - ${media.toolchain.dir}/aarch64-linux.cmake + ${jni.toolchain.dir}/aarch64-linux-clang.cmake diff --git a/webrtc-java-media/src/main/cpp/CMakeLists.txt b/webrtc-java-media/src/main/cpp/CMakeLists.txt index bd3268e8..ec42c18f 100644 --- a/webrtc-java-media/src/main/cpp/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/CMakeLists.txt @@ -59,6 +59,39 @@ target_include_directories(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME} PRIVATE ffmpeg) if(LINUX) + # + # The toolchain file builds against the Chromium sysroot, so that this + # library needs no newer glibc than webrtc-java's does: a machine that can + # run one can run the other. It also compiles with -nostdinc++, because + # everything Linux in this project uses the libc++ that WebRTC bundles + # rather than whatever the build machine happens to have. + # + # That libc++ is installed beside WebRTC itself, so webrtc-jni has to have + # been built before this module on Linux. It is the one thing this module + # takes from that build, and it is a C++ standard library, not any part of + # WebRTC: no WebRTC symbol is linked here, and the two native libraries + # still meet only through the C function table. + # + set(LIBCPP_CONFIG_DIR "${WEBRTC_INSTALL_DIR}/include/third_party/libc++") + set(LIBCPP_INCLUDE_DIR "${LIBCPP_CONFIG_DIR}/include") + set(LIBCPP_ABI_INCLUDE_DIR "${WEBRTC_INSTALL_DIR}/include/third_party/libc++abi/include") + + if(NOT EXISTS "${LIBCPP_INCLUDE_DIR}") + message(FATAL_ERROR + "No libc++ in ${LIBCPP_INCLUDE_DIR}. This module builds against the one WebRTC " + "bundles, so webrtc-jni has to be built first, or WEBRTC_INSTALL_DIR has to point " + "at where it installed WebRTC.") + endif() + + # The first carries __config_site, which the headers in the second need. + target_include_directories(${PROJECT_NAME} PRIVATE "${LIBCPP_CONFIG_DIR}") + target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE + "${LIBCPP_INCLUDE_DIR}" "${LIBCPP_ABI_INCLUDE_DIR}") + + target_link_directories(${PROJECT_NAME} PRIVATE "${WEBRTC_INSTALL_DIR}/lib") + target_link_libraries(${PROJECT_NAME} PRIVATE + -static-libgcc -stdlib=libc++ -lc++ -lc++abi) + target_link_libraries(${PROJECT_NAME} PRIVATE dl pthread) endif() diff --git a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt index 5fbc97c7..2f4cf5e1 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -246,22 +246,29 @@ else() # # The architecture under the name FFmpeg knows it by. - if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") - set(FFMPEG_ARCH x86_64) - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") - if(APPLE) - # Apple's clang knows this architecture as arm64 and rejects - # -arch aarch64 outright, which configure reports only as being - # unable to create an executable. - set(FFMPEG_ARCH arm64) - else() - set(FFMPEG_ARCH aarch64) + function(ffmpeg_arch_name PROCESSOR RESULT) + set(NAME "") + + if("${PROCESSOR}" MATCHES "^(x86_64|AMD64|amd64)$") + set(NAME x86_64) + elseif("${PROCESSOR}" MATCHES "^(aarch64|arm64|ARM64)$") + if(APPLE) + # Apple's clang knows this architecture as arm64 and rejects + # -arch aarch64 outright, which configure reports only as + # being unable to create an executable. + set(NAME arm64) + else() + set(NAME aarch64) + endif() + elseif("${PROCESSOR}" MATCHES "^(arm|armv7.*|armhf)$") + set(NAME arm) endif() - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm|armv7.*|armhf)$") - set(FFMPEG_ARCH arm) - else() - set(FFMPEG_ARCH "") - endif() + + set(${RESULT} "${NAME}" PARENT_SCOPE) + endfunction() + + ffmpeg_arch_name("${CMAKE_SYSTEM_PROCESSOR}" FFMPEG_ARCH) + ffmpeg_arch_name("${CMAKE_HOST_SYSTEM_PROCESSOR}" FFMPEG_HOST_ARCH) if(WIN32) set(FFMPEG_TARGET_OS win64) @@ -308,7 +315,20 @@ else() list(APPEND FFMPEG_CONFIGURE_ARGS --sysroot=${CMAKE_SYSROOT}) endif() + # CMake reports a cross build whenever a toolchain file names a system, + # even one that matches the host, and every toolchain file here does. What + # decides this for FFmpeg is narrower: whether it can run what it builds, + # which is a question about the architecture and the operating system. + set(FFMPEG_CROSS_COMPILING FALSE) + if(CMAKE_CROSSCOMPILING) + if(NOT CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME + OR NOT FFMPEG_ARCH STREQUAL FFMPEG_HOST_ARCH) + set(FFMPEG_CROSS_COMPILING TRUE) + endif() + endif() + + if(FFMPEG_CROSS_COMPILING) list(APPEND FFMPEG_CONFIGURE_ARGS --enable-cross-compile --target-os=${FFMPEG_TARGET_OS}) diff --git a/webrtc-java-media/src/main/cpp/toolchain/aarch32-linux.cmake b/webrtc-java-media/src/main/cpp/toolchain/aarch32-linux.cmake deleted file mode 100644 index d87686cb..00000000 --- a/webrtc-java-media/src/main/cpp/toolchain/aarch32-linux.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Cross build of the media module for arm-linux-gnueabihf. -# -# Deliberately not webrtc-jni's toolchain file for this target. That one -# compiles with -nostdinc++ and links WebRTC's own libc++, because webrtc-jni -# has to match WebRTC's C++ ABI exactly. This module links no C++ ABI at all, -# only FFmpeg's C interface and the JVM's, so it wants an ordinary compiler -# with an ordinary standard library, which is what the distribution's cross -# toolchain is. -# - -set(CMAKE_SYSTEM_NAME Linux) -set(CMAKE_SYSTEM_PROCESSOR arm) - -set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) -set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) - -# The host's binutils cannot read a binary for this architecture, which shows -# up as "unable to recognise the format of the input file" the moment anything -# tries to strip one. -set(CMAKE_AR arm-linux-gnueabihf-ar) -set(CMAKE_RANLIB arm-linux-gnueabihf-ranlib) -set(CMAKE_STRIP arm-linux-gnueabihf-strip) diff --git a/webrtc-java-media/src/main/cpp/toolchain/aarch64-linux.cmake b/webrtc-java-media/src/main/cpp/toolchain/aarch64-linux.cmake deleted file mode 100644 index 2095ce51..00000000 --- a/webrtc-java-media/src/main/cpp/toolchain/aarch64-linux.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Cross build of the media module for aarch64-linux-gnu. -# -# Deliberately not webrtc-jni's toolchain file for this target. That one -# compiles with -nostdinc++ and links WebRTC's own libc++, because webrtc-jni -# has to match WebRTC's C++ ABI exactly. This module links no C++ ABI at all, -# only FFmpeg's C interface and the JVM's, so it wants an ordinary compiler -# with an ordinary standard library, which is what the distribution's cross -# toolchain is. -# - -set(CMAKE_SYSTEM_NAME Linux) -set(CMAKE_SYSTEM_PROCESSOR aarch64) - -set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) -set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) - -# The host's binutils cannot read a binary for this architecture, which shows -# up as "unable to recognise the format of the input file" the moment anything -# tries to strip one. -set(CMAKE_AR aarch64-linux-gnu-ar) -set(CMAKE_RANLIB aarch64-linux-gnu-ranlib) -set(CMAKE_STRIP aarch64-linux-gnu-strip) From 9e7f5574ce1cad204681d7aab798634ba5a3213e Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Tue, 22 Sep 2026 00:15:03 +0200 Subject: [PATCH 10/12] ci: run the media tests against the natives that were cross compiled The test-natives lane existed to prove that a cross compiled native library actually runs on the machine it was built for, and it only ever did that for webrtc-java. The media natives were built, packaged, uploaded nowhere and never loaded on a real arm or arm64 machine, so nothing said whether FFmpeg and the media library work there or merely compile. Both jars are uploaded now. The lane installs webrtc-java's as before, and unpacks the media one into the directory the media module's tests already read natives from, which is where its own build collects them. Nothing else had to be taught where to look. The native build is skipped there with -DskipNativeBuild, which turns the two CMake executions off by moving them to no phase. Building FFmpeg again on a runner whose whole job is to run it would prove nothing and cost a quarter of an hour. webrtc is installed rather than only tested, so that the media module resolves it from the repository. Both in one reactor does not work, and the reason is worth writing down because it is the second time it has come up: Maven substitutes a module's own output for a dependency on it, and a classifier artifact of that same module, which is where the natives live, never reaches the class path. That is the same shape as the failure that broke the examples lane. Verified by doing locally what the lane does: unpacking the classifier jar into target/natives and running both commands. 168 tests in webrtc, 19 in webrtc-java-media, no CMake in the second, and the media tests load the unpacked libraries rather than any this machine built. The first attempt, both modules in one reactor, failed exactly as described above, which is how the reason is known rather than guessed. --- .github/actions/test-natives/action.yml | 41 +++++++++++++++++++++++-- .github/workflows/build.yml | 8 +++-- webrtc-java-media/pom.xml | 25 +++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/.github/actions/test-natives/action.yml b/.github/actions/test-natives/action.yml index 3866de00..5f764ebf 100644 --- a/.github/actions/test-natives/action.yml +++ b/.github/actions/test-natives/action.yml @@ -76,13 +76,50 @@ runs: - name: Install the natives into the local repository run: | VERSION=$(sed -n '0,//s|.*\([^<]*\).*|\1|p' pom.xml) - JAR=$(ls natives/webrtc-java-*.jar | head -n 1) + JAR=$(find natives -path '*webrtc-jni*' -name 'webrtc-java-*.jar' | head -n 1) echo "Installing $JAR as webrtc-java:$VERSION:${{ inputs.classifier }}" mvn -B -q install:install-file -Dfile="$JAR" -DgroupId=dev.onvoid.webrtc \ -DartifactId=webrtc-java -Dversion="$VERSION" -Dpackaging=jar \ -Dclassifier=${{ inputs.classifier }} shell: bash + # The media module reads its natives from the directory its own build + # collects them into, which is also how they are found when it builds them + # itself. Unpacking the jar there is all it takes to point the tests at the + # ones this lane is meant to exercise. + - name: Unpack the media natives where the tests look for them + run: | + MEDIA_JAR=$(find natives -name 'webrtc-java-media-*-${{ inputs.classifier }}.jar' | head -n 1) + + if [ -z "$MEDIA_JAR" ]; then + echo "No webrtc-java-media natives for ${{ inputs.classifier }} in the artifact" >&2 + exit 1 + fi + + MEDIA_JAR="$PWD/$MEDIA_JAR" + echo "Unpacking $MEDIA_JAR" + + mkdir -p webrtc-java-media/target/natives + cd webrtc-java-media/target/natives + # jar rather than unzip: the JDK is set up above, and unzip is not on + # every runner image. + "$JAVA_HOME/bin/jar" xf "$MEDIA_JAR" + rm -rf META-INF + ls -l + shell: bash + + # webrtc is installed rather than only tested, so that the media module + # below resolves it from the repository. Putting both in one reactor does + # not work: Maven then substitutes the module's own output for the + # dependency, and webrtc-java's natives, which are a classifier artifact of + # that same module, never reach the class path. Installing is seconds, and + # javadoc and sources belong to the release profile rather than to this. - name: Test - run: mvn -B -pl webrtc test -P${{ inputs.profile }} + run: mvn -B -pl webrtc install -P${{ inputs.profile }} + shell: bash + + # The native build is off: these natives were cross compiled by another + # job, and this runner is here to run them, not to build them again. + - name: Test the media natives + run: mvn -B -pl webrtc-java-media test -DskipNativeBuild -P${{ inputs.profile }} shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 386087dc..4de02cd7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: natives-${{ matrix.platform.name }} - path: webrtc-jni/target/webrtc-java-*.jar + path: | + webrtc-jni/target/webrtc-java-*.jar + webrtc-java-media/target/webrtc-java-media-*.jar if-no-files-found: error build-linux: @@ -97,7 +99,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: natives-${{ matrix.platform.name }} - path: webrtc-jni/target/webrtc-java-*.jar + path: | + webrtc-jni/target/webrtc-java-*.jar + webrtc-java-media/target/webrtc-java-media-*.jar if-no-files-found: error build-macos: diff --git a/webrtc-java-media/pom.xml b/webrtc-java-media/pom.xml index dd1c178f..e348cad2 100644 --- a/webrtc-java-media/pom.xml +++ b/webrtc-java-media/pom.xml @@ -24,6 +24,13 @@ the WebRTC checkout is reused. --> ${user.home}/ffmpeg/${platform.classifier} Release + + process-sources + compile @@ -76,6 +83,7 @@ cmake-generate + ${cmake.generate.phase} generate @@ -109,6 +117,7 @@ cmake-compile + ${cmake.compile.phase} compile @@ -170,6 +179,22 @@ + + + skip-native-build + + + skipNativeBuild + + + + none + none + + + From fb82ac119ee64c9023062e52e4600143157382cf Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Tue, 22 Sep 2026 00:54:28 +0200 Subject: [PATCH 11/12] fix: give the arm Linux builds the libc++ settings their toolchains omit The two arm lanes failed compiling the module, every translation unit dying inside libc++ before it reached a line of ours: _LIBCPP_HARDENING_MODE_DEFAULT is not defined. This definition should be set at configuration time linux-x86_64 passed, and the difference is in the toolchain files: x86_64-linux-clang.cmake sets -nostdinc++ and the hardening mode itself, while aarch64-linux-clang.cmake and aarch32-linux-clang.cmake set neither. webrtc-jni never notices, because it links the webrtc target and takes both from its PUBLIC flags. Nothing carries them to a module that links no WebRTC target, which is this one by design. So the module sets them on itself. The bundled libc++ has wanted its hardening mode chosen at configuration time since branch-heads/7977, and -nostdinc++ is what keeps clang from putting the GCC libstdc++ headers it finds in the sysroot on the implicit search path, where they collide with the bundled ones. This is the same lesson as the last two rounds, arriving once more: webrtc-jni's toolchain files describe half of an arrangement, and the other half lives on the webrtc target. Reading only the file leaves the half that matters behind. Windows is untouched and still builds with 19 tests green. The arm lanes wait on CI. --- webrtc-java-media/src/main/cpp/CMakeLists.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/webrtc-java-media/src/main/cpp/CMakeLists.txt b/webrtc-java-media/src/main/cpp/CMakeLists.txt index ec42c18f..f338320c 100644 --- a/webrtc-java-media/src/main/cpp/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/CMakeLists.txt @@ -88,6 +88,23 @@ if(LINUX) target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE "${LIBCPP_INCLUDE_DIR}" "${LIBCPP_ABI_INCLUDE_DIR}") + # These two belong with those include directories and have to be set here + # rather than left to the toolchain file, which only the x86_64 one does. + # webrtc-jni does not notice, because it links the webrtc target and picks + # them up from its PUBLIC flags; nothing carries them to this module. + # + # The bundled libc++ has wanted the hardening mode chosen at configuration + # time since branch-heads/7977, and without it every translation unit + # fails in __config before it reaches a line of ours. + target_compile_definitions(${PROJECT_NAME} + PRIVATE _LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE) + + # Clang finds the GCC installation inside the cross sysroots and puts its + # libstdc++ headers on the implicit search path, where they collide with + # the bundled libc++. Excluding the implicit C++ directories leaves the + # bundled headers serving C++ on their own. + target_compile_options(${PROJECT_NAME} PRIVATE -nostdinc++) + target_link_directories(${PROJECT_NAME} PRIVATE "${WEBRTC_INSTALL_DIR}/lib") target_link_libraries(${PROJECT_NAME} PRIVATE -static-libgcc -stdlib=libc++ -lc++ -lc++abi) From 275158cf5ac59817d9d94a90e6d4f6c21d7e8d38 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Tue, 22 Sep 2026 08:21:45 +0200 Subject: [PATCH 12/12] fix: run the native tests without packaging the webrtc module All three test-natives lanes failed, and neither half of what I wrote survived contact with that runner. Installing webrtc so that the media module could resolve it walked into its package phase, where an antrun task attaches the host natives from webrtc-jni/target. On a runner that only downloaded a jar there is no such directory: attach-artifact on project webrtc-java: File does not exist: ../webrtc-jni/target/webrtc-java-0.19.0-SNAPSHOT-linux-aarch64.jar That is why the original lane stopped at test rather than going further, and it stops there again. Both modules are back in one reactor, which is what makes webrtc-java resolvable, and the reason that failed before is now handled where it belongs. webrtc-java's natives are a classifier artifact of a module in that same reactor, and Maven answers a dependency on such a thing with the module's output directory, which holds no native library at all. Both libraries are therefore unpacked into the directory the media tests already read from, and are found as files rather than resolved as artifacts. Verified by the comparison that shows it: the same reactor command fails with NoClassDefFound on AudioDeviceModule when only the media natives are staged there, and passes with 168 and 19 tests when webrtc's are staged beside them. The local repository was identical in both runs, so the staging is what carries them. --- .github/actions/test-natives/action.yml | 36 ++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/actions/test-natives/action.yml b/.github/actions/test-natives/action.yml index 5f764ebf..02459615 100644 --- a/.github/actions/test-natives/action.yml +++ b/.github/actions/test-natives/action.yml @@ -83,13 +83,17 @@ runs: -Dclassifier=${{ inputs.classifier }} shell: bash - # The media module reads its natives from the directory its own build - # collects them into, which is also how they are found when it builds them - # itself. Unpacking the jar there is all it takes to point the tests at the - # ones this lane is meant to exercise. - - name: Unpack the media natives where the tests look for them + # Both native libraries go into the directory the media module's tests + # already read from, which is where its own build collects them. That is + # what puts them on the class path, and it is also what makes the reactor + # below work: webrtc-java's natives are a classifier artifact of a module + # that is in that reactor, and Maven answers a dependency on such a thing + # with the module's output directory, which holds no native library at + # all. Coming from a directory instead, they are simply found. + - name: Unpack the natives where the tests look for them run: | MEDIA_JAR=$(find natives -name 'webrtc-java-media-*-${{ inputs.classifier }}.jar' | head -n 1) + WEBRTC_JAR=$(find natives -path '*webrtc-jni*' -name 'webrtc-java-*.jar' | head -n 1) if [ -z "$MEDIA_JAR" ]; then echo "No webrtc-java-media natives for ${{ inputs.classifier }} in the artifact" >&2 @@ -97,29 +101,25 @@ runs: fi MEDIA_JAR="$PWD/$MEDIA_JAR" - echo "Unpacking $MEDIA_JAR" + WEBRTC_JAR="$PWD/$WEBRTC_JAR" mkdir -p webrtc-java-media/target/natives cd webrtc-java-media/target/natives # jar rather than unzip: the JDK is set up above, and unzip is not on # every runner image. "$JAVA_HOME/bin/jar" xf "$MEDIA_JAR" + "$JAVA_HOME/bin/jar" xf "$WEBRTC_JAR" rm -rf META-INF ls -l shell: bash - # webrtc is installed rather than only tested, so that the media module - # below resolves it from the repository. Putting both in one reactor does - # not work: Maven then substitutes the module's own output for the - # dependency, and webrtc-java's natives, which are a classifier artifact of - # that same module, never reach the class path. Installing is seconds, and - # javadoc and sources belong to the release profile rather than to this. - - name: Test - run: mvn -B -pl webrtc install -P${{ inputs.profile }} - shell: bash - + # One reactor for both, so that webrtc-java-media resolves webrtc-java + # from it. Testing rather than installing: webrtc attaches the host + # natives from webrtc-jni/target during its package phase, and on a runner + # that only downloaded them there is no such directory. + # # The native build is off: these natives were cross compiled by another # job, and this runner is here to run them, not to build them again. - - name: Test the media natives - run: mvn -B -pl webrtc-java-media test -DskipNativeBuild -P${{ inputs.profile }} + - name: Test + run: mvn -B -pl webrtc,webrtc-java-media test -DskipNativeBuild -P${{ inputs.profile }} shell: bash