From 78afb2177a0e7c9ba0ce92595c2677146971967d Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 22:50:27 +0200 Subject: [PATCH 1/5] ci: move actions still on Node.js 20 to their Node.js 24 majors GitHub forces Node.js 20 actions onto Node.js 24 and flags each run with a deprecation annotation. Bump cache to v6, upload-artifact to v7, download-artifact to v8, setup-python to v7, configure-pages to v6, deploy-pages to v5 and upload-pages-artifact to v5 (which wraps upload-artifact v7). None of their breaking changes affect how these workflows call them: artifacts are downloaded by name/pattern, not ID, and pip-install is not used. --- .github/actions/build-macos-x86_64/action.yml | 4 ++-- .github/actions/build/action.yml | 6 +++--- .github/actions/prepare-windows/action.yml | 2 +- .github/actions/release-macos-x86_64/action.yml | 6 +++--- .github/actions/release/action.yml | 8 ++++---- .github/actions/test-natives/action.yml | 2 +- .github/workflows/build.yml | 4 ++-- .github/workflows/pages.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/actions/build-macos-x86_64/action.yml b/.github/actions/build-macos-x86_64/action.yml index 5320fe5e..7536707f 100644 --- a/.github/actions/build-macos-x86_64/action.yml +++ b/.github/actions/build-macos-x86_64/action.yml @@ -28,14 +28,14 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} 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: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 99ca8e90..887b1aa4 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -28,7 +28,7 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} @@ -39,14 +39,14 @@ runs: shell: bash - name: Set up FFmpeg cache - uses: actions/cache@v4 + uses: actions/cache@v6 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 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} diff --git a/.github/actions/prepare-windows/action.yml b/.github/actions/prepare-windows/action.yml index dd0e96a5..692cf2bf 100644 --- a/.github/actions/prepare-windows/action.yml +++ b/.github/actions/prepare-windows/action.yml @@ -7,7 +7,7 @@ runs: steps: - name: Set up Python 3.11 if: false - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.11" diff --git a/.github/actions/release-macos-x86_64/action.yml b/.github/actions/release-macos-x86_64/action.yml index c14d53d5..ba402521 100644 --- a/.github/actions/release-macos-x86_64/action.yml +++ b/.github/actions/release-macos-x86_64/action.yml @@ -20,14 +20,14 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} 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: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} @@ -52,7 +52,7 @@ runs: # deploy. The native library jar is handed to the aggregating publish-central # job instead, which attaches every platform to a single deployment. - name: Upload native library jar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ inputs.platform-name }} path: webrtc-jni/target/webrtc-java-*.jar diff --git a/.github/actions/release/action.yml b/.github/actions/release/action.yml index 0c6fe4af..fc4a5a6b 100644 --- a/.github/actions/release/action.yml +++ b/.github/actions/release/action.yml @@ -19,7 +19,7 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} @@ -30,14 +30,14 @@ runs: shell: bash - name: Set up FFmpeg cache - uses: actions/cache@v4 + uses: actions/cache@v6 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 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} @@ -71,7 +71,7 @@ runs: # deploy. The native library jar is handed to the aggregating publish-central # job instead, which attaches every platform to a single deployment. - name: Upload native library jar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ inputs.platform-name }} path: webrtc-jni/target/webrtc-java-*.jar diff --git a/.github/actions/test-natives/action.yml b/.github/actions/test-natives/action.yml index 02459615..6a09796d 100644 --- a/.github/actions/test-natives/action.yml +++ b/.github/actions/test-natives/action.yml @@ -34,7 +34,7 @@ runs: using: "composite" steps: - name: Download natives - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: natives-${{ inputs.platform-name }} path: natives diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4de02cd7..1a195411 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: - name: Upload natives for native testing if: matrix.platform.name == 'windows_arm64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ matrix.platform.name }} path: | @@ -96,7 +96,7 @@ jobs: - name: Upload natives for native testing if: matrix.platform.name == 'linux_arm' || matrix.platform.name == 'linux_arm64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ matrix.platform.name }} path: | diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9eb41846..e4a23ee7 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -32,7 +32,7 @@ jobs: cache-dependency-path: docs/package-lock.json - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Install dependencies run: | @@ -45,7 +45,7 @@ jobs: npm run build - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: docs/.vitepress/dist @@ -59,4 +59,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v5 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33085969..2ee8ba10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -278,7 +278,7 @@ jobs: fetch-tags: true - name: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-publish-${{ hashFiles('**/pom.xml') }} @@ -311,7 +311,7 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" - name: Download the native library jars of all platforms - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: natives-* path: natives @@ -380,7 +380,7 @@ jobs: - name: Upload the deployment bundle for inspection if: ${{ inputs.dryRun }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: deployment-bundle-dry-run path: | From d8f5dc34b0e4bc610f9812892732ff01d155fb01 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 23:03:22 +0200 Subject: [PATCH 2/5] build: silence the module-not-found warning for the qualified internal export webrtc.java exports dev.onvoid.webrtc.internal only to webrtc.java.media. That module depends on webrtc.java, so it is never observable while webrtc.java compiles, and javac reports [module] module not found on every build, which CI surfaces as an annotation. The export is intentional and still takes effect at run time, so suppress the lint on the module declaration rather than disabling it for the whole build. --- webrtc/src/main/java/module-info.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webrtc/src/main/java/module-info.java b/webrtc/src/main/java/module-info.java index 999476b6..743fff91 100644 --- a/webrtc/src/main/java/module-info.java +++ b/webrtc/src/main/java/module-info.java @@ -1,3 +1,6 @@ +// The qualified export below names webrtc.java.media, which depends on this +// module and so is never on the module path while this module compiles. +@SuppressWarnings("module") module webrtc.java { requires java.desktop; From 16cbeaa1a73a424c9a26e84fa58da001da0f48ff Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 23:38:42 +0200 Subject: [PATCH 3/5] fix: report data channel send observer exceptions through the uncaught exception handler An exception thrown by an RTCDataChannelSendObserver was reported with ExceptionDescribe. HotSpot prints its "Exception in thread" prefix straight to native stderr, without a newline, and the stack trace to System.err, so the report was split across two streams and the prefix was left on an unterminated line. In CI the next native write, LoggingTests' debug log line, landed on that line, and setup-java's java problem matcher turned it into a "logToDebug at ERROR" error annotation. Hand the exception to the calling thread's uncaught exception handler instead, as the JVM does for an exception escaping a Java thread. The whole report now goes through System.err, and applications can install their own handler. The tests that throw from an observer on purpose now capture the exception in a default handler and assert it arrived. --- .../src/api/RTCDataChannelSendObserver.cpp | 41 ++++++++++++++++++- .../RTCDataChannelSendCompletionTests.java | 32 ++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp index b8e1f34d..9758efc3 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp @@ -3,6 +3,44 @@ #include "JavaString.h" #include "JNI_WebRTC.h" +namespace +{ + // Hands an exception thrown by the observer to the calling thread's uncaught + // exception handler, as the JVM does for an exception escaping a Java thread. + // ExceptionDescribe would print the "Exception in thread" prefix straight to + // native stderr and the stack trace to System.err, splitting the report + // across two streams and leaving the prefix on an unterminated line. + void ReportUncaughtException(JNIEnv * env, jthrowable exception) + { + jni::JavaLocalRef threadClass(env, env->FindClass("java/lang/Thread")); + jni::JavaLocalRef handlerClass(env, env->FindClass("java/lang/Thread$UncaughtExceptionHandler")); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return; + } + + jmethodID currentThread = env->GetStaticMethodID(threadClass.get(), "currentThread", "()Ljava/lang/Thread;"); + jmethodID getHandler = env->GetMethodID(threadClass.get(), "getUncaughtExceptionHandler", + "()Ljava/lang/Thread$UncaughtExceptionHandler;"); + jmethodID uncaughtException = env->GetMethodID(handlerClass.get(), "uncaughtException", + "(Ljava/lang/Thread;Ljava/lang/Throwable;)V"); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return; + } + + jni::JavaLocalRef thread(env, env->CallStaticObjectMethod(threadClass.get(), currentThread)); + jni::JavaLocalRef handler(env, env->CallObjectMethod(thread.get(), getHandler)); + if (!env->ExceptionCheck() && handler.get()) { + env->CallVoidMethod(handler.get(), uncaughtException, thread.get(), exception); + } + // Like the JVM, ignore an exception thrown by the handler itself. + if (env->ExceptionCheck()) { + env->ExceptionClear(); + } + } +} + namespace jni { RTCDataChannelSendObserver::RTCDataChannelSendObserver(JNIEnv * env, jobject observer) : @@ -58,8 +96,9 @@ namespace jni } // A Java exception must not escape into WebRTC's network task or a destructor. if (env->ExceptionCheck()) { - env->ExceptionDescribe(); + JavaLocalRef exception(env, env->ExceptionOccurred()); env->ExceptionClear(); + ReportUncaughtException(env, exception.get()); } } diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java index cf12c1ed..80ff23d9 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java @@ -58,7 +58,8 @@ void rejectsInvalidArgumentsWithoutCallingObserver() { @Test void callbackExceptionDoesNotStopLaterSends() throws Exception { - try (TestDataChannelPair pair = new TestDataChannelPair(factory)) { + try (UncaughtCapture uncaught = new UncaughtCapture(); + TestDataChannelPair pair = new TestDataChannelPair(factory)) { pair.connect(); CountDownLatch entered = new CountDownLatch(1); RTCDataChannelBuffer buffer = new RTCDataChannelBuffer(ByteBuffer.allocate(1), true); @@ -80,12 +81,14 @@ public void onFailure(String error) { pair.sender.sendAsync(buffer, next); assertNull(next.result.get(5, TimeUnit.SECONDS)); assertEquals(1, next.calls.get()); + uncaught.assertReceived("Send observer test exception"); } } @Test void failureCallbackExceptionDoesNotStopLaterSends() throws Exception { - try (TestDataChannelPair pair = new TestDataChannelPair(factory)) { + try (UncaughtCapture uncaught = new UncaughtCapture(); + TestDataChannelPair pair = new TestDataChannelPair(factory)) { CountDownLatch entered = new CountDownLatch(1); RTCDataChannelBuffer buffer = new RTCDataChannelBuffer(ByteBuffer.allocate(1), true); pair.sender.sendAsync(buffer, new RTCDataChannelSendObserver() { @@ -103,6 +106,7 @@ public void onFailure(String error) { }); assertTrue(entered.await(5, TimeUnit.SECONDS), "Observer was not called"); assertRejected(pair.sender); + uncaught.assertReceived("Send failure observer test exception"); } } @@ -196,4 +200,28 @@ public void onFailure(String error) { result.complete(error); } } + + /** + * Replaces the default uncaught exception handler while open, so an exception + * an observer throws on purpose is captured and asserted instead of printed. + */ + private static class UncaughtCapture implements AutoCloseable { + final CompletableFuture exception = new CompletableFuture<>(); + final Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler(); + + UncaughtCapture() { + Thread.setDefaultUncaughtExceptionHandler((thread, e) -> exception.complete(e)); + } + + void assertReceived(String message) throws Exception { + Throwable received = exception.get(5, TimeUnit.SECONDS); + assertEquals(IllegalStateException.class, received.getClass()); + assertEquals(message, received.getMessage()); + } + + @Override + public void close() { + Thread.setDefaultUncaughtExceptionHandler(previous); + } + } } From cf1f7269aefdbcdd7aea758aeddc42ab470d7016 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 23:59:52 +0200 Subject: [PATCH 4/5] ci: build the Linux natives with the Clang that the WebRTC branch pins prepare-linux installed a Clang 20 development snapshot (llvmorg-20-init) for the Linux toolchain files. The JNI code compiles against the libc++ headers installed by the WebRTC build, and those of branch-heads/7977 only support Clang 21 and later, so every Linux build printed "Libc++ only supports Clang 21 and later" for each translation unit (294 times on linux_arm alone). Install the package pinned by tools/clang/scripts/update.py on that branch (llvmorg-23-init-19482-g53d18800-1), the compiler WebRTC itself is built with, and note that the pin has to follow webrtc.branch. --- .github/actions/prepare-linux/action.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/actions/prepare-linux/action.yml b/.github/actions/prepare-linux/action.yml index 23f07c40..9fc88bfc 100644 --- a/.github/actions/prepare-linux/action.yml +++ b/.github/actions/prepare-linux/action.yml @@ -34,12 +34,16 @@ runs: # 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 - # Use a more stable version of Clang + # Chromium Clang to be used with the clang toolchain file. The JNI code + # compiles against the libc++ headers of the WebRTC build, which only + # support the Clang that WebRTC branch ships with, so this must be the + # package pinned by CLANG_REVISION and CLANG_SUB_REVISION in + # tools/clang/scripts/update.py of the webrtc.branch set in + # webrtc-jni/pom.xml. Update it whenever that branch changes. + CLANG_PACKAGE=clang-llvmorg-23-init-19482-g53d18800-1.tar.xz sudo mkdir -p /opt/clang - wget https://commondatastorage.googleapis.com/chromium-browser-clang/Linux_x64/clang-llvmorg-20-init-9764-gb81d8e90-72.tar.xz - sudo tar -xvf clang-llvmorg-20-init-9764-gb81d8e90-72.tar.xz -C /opt/clang + wget https://commondatastorage.googleapis.com/chromium-browser-clang/Linux_x64/$CLANG_PACKAGE + sudo tar -xvf $CLANG_PACKAGE -C /opt/clang shell: bash - name: Install required packages for x86-64 From 096e63b64c17f60e89b8896c5ae5646243233479 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 27 Sep 2026 00:20:30 +0200 Subject: [PATCH 5/5] build: remove a stray comment opener from JNI_RTCPeerConnection.h A duplicated "/*" before the getSignalingState declaration nested a comment opener inside a block comment, so every native build warned "'/*' within block comment [-Wcomment]" (once per platform in CI). --- webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h | 1 - 1 file changed, 1 deletion(-) diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h b/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h index fd3aaea9..32ac991a 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h @@ -151,7 +151,6 @@ extern "C" { JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addIceCandidate (JNIEnv *, jobject, jobject); - /* /* * Class: dev_onvoid_webrtc_RTCPeerConnection * Method: getSignalingState