From efd63c00c1fc919e793c33ca02b14da6e4148f56 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 20 Sep 2026 19:26:34 +0200 Subject: [PATCH 01/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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 From bceb24f3fc8d6f7718590288d25d28b9575bd432 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 00:21:35 +0200 Subject: [PATCH 13/33] fix: apply the encoder's adaptation requests to custom video sources CustomVideoSource extends AdaptedVideoTrackSource but handed every frame straight to OnFrame without calling AdaptFrame. The encoder's requests for a lower resolution or frame rate never took effect, so when the bitrate could not carry the pushed frames, the encoder kept receiving them at full size and dropped most of them instead. A 1080p file played through a peer connection arrived at around 5 fps. Frames are now adapted the way the desktop and camera sources already do: dropped when the requested frame rate says so, and cropped and scaled to the requested size otherwise. --- docs/guide/video/custom-video-source.md | 1 + .../cpp/src/media/video/CustomVideoSource.cpp | 26 ++++ .../CustomVideoSourceAdaptationTests.java | 137 ++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java diff --git a/docs/guide/video/custom-video-source.md b/docs/guide/video/custom-video-source.md index 9753ac13..0b3da97e 100644 --- a/docs/guide/video/custom-video-source.md +++ b/docs/guide/video/custom-video-source.md @@ -78,6 +78,7 @@ When pushing video frames, you need to consider the following: ### Resolution - Common resolutions: 320x240, 640x480, 1280x720, 1920x1080 - Higher resolutions require more bandwidth and processing power +- Push frames at their full size: when the bitrate cannot carry them, the source scales them down (and, if asked, drops some) before they reach the encoder, the way a camera source does, and scales back up once the bandwidth allows. Local sinks on the track see the adapted frames too. ### Frame Rate - Common frame rates: 15, 24, 30, 60 fps diff --git a/webrtc-jni/src/main/cpp/src/media/video/CustomVideoSource.cpp b/webrtc-jni/src/main/cpp/src/media/video/CustomVideoSource.cpp index 20890223..096a204d 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/CustomVideoSource.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/CustomVideoSource.cpp @@ -66,9 +66,35 @@ namespace jni void CustomVideoSource::DeliverFrame(const webrtc::VideoFrame& frame, int64_t timestamp_us, int64_t ntp_time_ms) { + const int width = frame.width(); + const int height = frame.height(); + + int adapted_width; + int adapted_height; + int crop_width; + int crop_height; + int crop_x; + int crop_y; + + // The encoder asks for less when the bitrate cannot carry what it is + // given: a lower resolution, or fewer frames. Those requests reach the + // source rather than the encoder, and only this call applies them. + // Without it the encoder keeps receiving full-size frames it cannot + // afford and drops most of them, which plays as a few frames a second. + if (!AdaptFrame(width, height, timestamp_us, &adapted_width, &adapted_height, + &crop_width, &crop_height, &crop_x, &crop_y)) { + // Dropped to keep to the frame rate asked for. + return; + } + // Create frame with proper timestamp webrtc::VideoFrame timestamped_frame = frame; + if (adapted_width != width || adapted_height != height) { + timestamped_frame.set_video_frame_buffer(frame.video_frame_buffer()->CropAndScale( + crop_x, crop_y, crop_width, crop_height, adapted_width, adapted_height)); + } + timestamped_frame.set_timestamp_us(timestamp_us); // Set RTP timestamp (90kHz clock). The encoder recomputes this from diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java new file mode 100644 index 00000000..f39e3ead --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java @@ -0,0 +1,137 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import dev.onvoid.webrtc.media.video.CustomVideoSource; +import dev.onvoid.webrtc.media.video.NativeI420Buffer; +import dev.onvoid.webrtc.media.video.VideoFrame; +import dev.onvoid.webrtc.media.video.VideoTrack; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Tests that a CustomVideoSource adapts what it delivers to what the encoder + * asks for, the way a camera or a desktop source does. The encoder lowers the + * resolution when the bitrate cannot carry the frames it is given; a source + * that ignores that leaves the encoder dropping most frames instead, which + * shows up as a video that plays at a few frames a second. + */ +@Execution(ExecutionMode.SAME_THREAD) +class CustomVideoSourceAdaptationTests extends TestBase { + + private static final int WIDTH = 1280; + private static final int HEIGHT = 720; + + + @Test + void lowBitrateScalesDown() throws Exception { + CustomVideoSource source = new CustomVideoSource(); + VideoTrack track = factory.createVideoTrack("video", source); + + TestPeerConnection caller = new TestPeerConnection(factory); + TestPeerConnection callee = new TestPeerConnection(factory); + + RTCRtpSender sender = caller.getPeerConnection() + .addTrack(track, Collections.singletonList("stream")); + + caller.setRemotePeerConnection(callee); + callee.setRemotePeerConnection(caller); + + callee.setRemoteDescription(caller.createOffer()); + caller.setRemoteDescription(callee.createAnswer()); + + caller.waitUntilConnected(); + callee.waitUntilConnected(); + + // Far too little for 720p of noise, so the encoder has to give way. + RTCRtpSendParameters parameters = sender.getParameters(); + parameters.encodings.get(0).maxBitrate = 150_000; + sender.setParameters(parameters); + + Random random = new Random(1); + byte[] noise = new byte[WIDTH * HEIGHT]; + long frameUs = 1_000_000 / 30; + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); + long nextStatsNs = 0; + long sentWidth = WIDTH; + + try { + while (System.nanoTime() < deadline && sentWidth >= WIDTH) { + NativeI420Buffer buffer = NativeI420Buffer.allocate(WIDTH, HEIGHT); + + // Noise, because a flat picture costs next to nothing to encode + // and would never make the encoder ask for less. + random.nextBytes(noise); + ByteBuffer y = buffer.getDataY(); + y.put(noise, 0, y.remaining()); + + VideoFrame frame = new VideoFrame(buffer, 0); + source.pushFrame(frame); + frame.release(); + + if (System.nanoTime() > nextStatsNs) { + nextStatsNs = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(500); + sentWidth = sentFrameWidth(caller.getPeerConnection()); + } + + Thread.sleep(frameUs / 1000); + } + + assertTrue(sentWidth < WIDTH, + "the encoder kept receiving " + WIDTH + "x" + HEIGHT + " frames"); + } + finally { + sender.dispose(); + caller.close(); + callee.close(); + track.dispose(); + source.dispose(); + } + } + + /** + * Returns the width of the frames the sender encodes, or the full width + * while it has not reported one yet. + */ + private static long sentFrameWidth(RTCPeerConnection peerConnection) throws Exception { + CompletableFuture future = new CompletableFuture<>(); + peerConnection.getStats(future::complete); + + for (RTCStats stats : future.get(2, TimeUnit.SECONDS).getStats().values()) { + if (stats.getType() == RTCStatsType.OUTBOUND_RTP + && "video".equals(stats.getAttributes().get("kind"))) { + Object width = stats.getAttributes().get("frameWidth"); + + if (width instanceof Number) { + return ((Number) width).longValue(); + } + } + } + + return WIDTH; + } +} From da1f331690613df3fae6456ab9c240558a4ac708 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 00:22:03 +0200 Subject: [PATCH 14/33] feat: decode the video and audio AVI files commonly carry The AVI demuxer was enabled, but not the decoders for what AVI files typically hold beyond MPEG-4: Microsoft's MPEG-4 variants (MPG4, MP42, DIV3), and MP2, AC-3 and ADPCM audio. Such a file opened, then failed as soon as playback tried to decode it. E-AC-3 is enabled alongside AC-3, whose decoder calls into its tables and which MSVC otherwise fails to link. A configure that found FFmpeg already installed skipped the build regardless of which components it had been built with, so a new decoder never reached an existing install. The install now records its version and components, and a mismatch rebuilds it. The test asset is the first quarter second of Big Buck Bunny as the AVI it is distributed in, cut without re-encoding. --- docs/guide/media/media-files.md | 4 +- .../cpp/dependencies/ffmpeg/CMakeLists.txt | 137 ++++++++++++------ .../webrtc/media/ffmpeg/MediaPlayerTest.java | 21 +++ .../webrtc/media/ffmpeg/MediaReaderTest.java | 31 +++- .../src/test/resources/media-test.avi | Bin 0 -> 78392 bytes 5 files changed, 142 insertions(+), 51 deletions(-) create mode 100644 webrtc-java-media/src/test/resources/media-test.avi diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index e727cde1..2c19a3ce 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -218,8 +218,8 @@ The FFmpeg build is deliberately small, and carries only what this module plays: | | | | --- | --- | | **Containers** | MP4 and MOV, Matroska and WebM, AVI, MPEG-TS, FLV, WAV, MP3, Ogg, FLAC, AAC | -| **Video** | H.264, H.265/HEVC, VP8, VP9, MPEG-4, MJPEG | -| **Audio** | AAC, MP3, Opus, Vorbis, FLAC, PCM | +| **Video** | H.264, H.265/HEVC, VP8, VP9, MPEG-4 (including Xvid and DivX), Microsoft MPEG-4 v1 to v3, MJPEG | +| **Audio** | AAC, MP3, MP2, AC-3, Opus, Vorbis, FLAC, PCM, MS and IMA ADPCM | Audio of any rate or layout is resampled to what WebRTC takes, which is 48 kHz 16-bit PCM in mono or stereo. Video that decodes to I420 — almost all 8-bit H.264, VP8, VP9 and MPEG-4 — reaches the encoder without being copied; anything else is converted first. 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 2f4cf5e1..879e1eef 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -8,8 +8,8 @@ project(ffmpeg) # # The build runs at CMake configure time and installs into FFMPEG_INSTALL_DIR, # the way the WebRTC dependency of webrtc-jni does. A configure that finds the -# libraries already installed there skips the whole thing, so only the first -# build of a platform pays for it. +# libraries already installed there, built with the same components, skips the +# whole thing, so only the first build of a platform pays for it. # # The configuration is deliberately LGPL only: no --enable-gpl and no # --enable-nonfree, and the libraries are shared so that they can be replaced, @@ -36,9 +36,80 @@ file(MAKE_DIRECTORY "${FFMPEG_INSTALL_DIR}") # twice: for the link line, and for the order the Java side loads them in. set(FFMPEG_LIBS avutil swresample swscale avcodec avformat) +# The parts of FFmpeg this module plays with. Everything else is left out. +set(FFMPEG_COMPONENTS + # Containers this module reads. + --enable-demuxer=mov + --enable-demuxer=matroska + --enable-demuxer=avi + --enable-demuxer=mpegts + --enable-demuxer=flv + --enable-demuxer=wav + --enable-demuxer=mp3 + --enable-demuxer=ogg + --enable-demuxer=flac + --enable-demuxer=aac + # Video this module decodes. + --enable-decoder=h264 + --enable-decoder=hevc + --enable-decoder=vp8 + --enable-decoder=vp9 + --enable-decoder=mpeg4 + --enable-decoder=mjpeg + # What AVI files carry besides MPEG-4: Microsoft's MPEG-4 variants, which + # is what an AVI tagged MPG4, MP42 or DIV3 holds. + --enable-decoder=msmpeg4v1 + --enable-decoder=msmpeg4v2 + --enable-decoder=msmpeg4v3 + # Audio this module decodes. + --enable-decoder=aac + --enable-decoder=mp3 + --enable-decoder=opus + --enable-decoder=vorbis + --enable-decoder=flac + --enable-decoder=pcm_s16le + --enable-decoder=pcm_s16be + --enable-decoder=pcm_u8 + --enable-decoder=pcm_f32le + # Audio common in AVI files and in MPEG transport streams. + --enable-decoder=mp2 + --enable-decoder=ac3 + # Not only for E-AC-3 itself: the AC-3 decoder calls into its tables, and + # MSVC, unlike GCC and clang, does not drop those calls when it is left + # out, so AC-3 alone fails to link on Windows. + --enable-decoder=eac3 + --enable-decoder=adpcm_ms + --enable-decoder=adpcm_ima_wav + # Parsers for the decoders above. + --enable-parser=h264 + --enable-parser=hevc + --enable-parser=vp8 + --enable-parser=vp9 + --enable-parser=mpeg4video + --enable-parser=mjpeg + --enable-parser=aac + --enable-parser=mpegaudio + --enable-parser=ac3 + --enable-parser=opus + --enable-parser=vorbis + --enable-parser=flac + # Local files only for now; network protocols follow once the TLS + # backends are wired up per platform. + --enable-protocol=file + --enable-protocol=pipe +) + +# Written next to an install once it is built, naming the components it was +# built with. Without it, an install from before a component was added would +# be taken as good, and a file needing that component would fail to play with +# nothing to say why. +set(FFMPEG_COMPONENTS_FILE "${FFMPEG_INSTALL_DIR}/components.txt") +string(REPLACE ";" "\n" FFMPEG_COMPONENTS_TEXT "${FFMPEG_VERSION};${FFMPEG_COMPONENTS}") + # # Decides whether FFmpeg has to be built, by looking for one of its headers -# and one of its libraries in the install directory. +# and one of its libraries in the install directory, and for a record of it +# having been built with the components listed above. # function(ffmpeg_is_installed RESULT) if(NOT EXISTS "${FFMPEG_INSTALL_DIR}/include/libavformat/avformat.h") @@ -46,6 +117,19 @@ function(ffmpeg_is_installed RESULT) return() endif() + if(NOT EXISTS "${FFMPEG_COMPONENTS_FILE}") + set(${RESULT} FALSE PARENT_SCOPE) + return() + endif() + + file(READ "${FFMPEG_COMPONENTS_FILE}" INSTALLED_COMPONENTS) + + if(NOT INSTALLED_COMPONENTS STREQUAL FFMPEG_COMPONENTS_TEXT) + message(STATUS "FFmpeg in ${FFMPEG_INSTALL_DIR} was built with other components, rebuilding") + set(${RESULT} FALSE PARENT_SCOPE) + return() + endif() + file(GLOB FOUND_LIBS "${FFMPEG_INSTALL_DIR}/lib/avformat*" "${FFMPEG_INSTALL_DIR}/lib/libavformat*" @@ -193,50 +277,7 @@ else() --disable-network --disable-everything --disable-autodetect - # Containers this module reads. - --enable-demuxer=mov - --enable-demuxer=matroska - --enable-demuxer=avi - --enable-demuxer=mpegts - --enable-demuxer=flv - --enable-demuxer=wav - --enable-demuxer=mp3 - --enable-demuxer=ogg - --enable-demuxer=flac - --enable-demuxer=aac - # Video this module decodes. - --enable-decoder=h264 - --enable-decoder=hevc - --enable-decoder=vp8 - --enable-decoder=vp9 - --enable-decoder=mpeg4 - --enable-decoder=mjpeg - # Audio this module decodes. - --enable-decoder=aac - --enable-decoder=mp3 - --enable-decoder=opus - --enable-decoder=vorbis - --enable-decoder=flac - --enable-decoder=pcm_s16le - --enable-decoder=pcm_s16be - --enable-decoder=pcm_u8 - --enable-decoder=pcm_f32le - # Parsers for the decoders above. - --enable-parser=h264 - --enable-parser=hevc - --enable-parser=vp8 - --enable-parser=vp9 - --enable-parser=mpeg4video - --enable-parser=mjpeg - --enable-parser=aac - --enable-parser=mpegaudio - --enable-parser=opus - --enable-parser=vorbis - --enable-parser=flac - # Local files only for now; network protocols follow once the TLS - # backends are wired up per platform. - --enable-protocol=file - --enable-protocol=pipe + ${FFMPEG_COMPONENTS} ) # @@ -475,6 +516,8 @@ set MSYS2_PATH_TYPE=inherit "${FFMPEG_BUILD_SCRIPT}.") endif() + file(WRITE "${FFMPEG_COMPONENTS_FILE}" "${FFMPEG_COMPONENTS_TEXT}") + ffmpeg_is_installed(FFMPEG_INSTALLED) if(NOT FFMPEG_INSTALLED) diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java index e0786de0..a3ac7269 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java @@ -128,6 +128,27 @@ void deliversWhatWebRtcTakes() throws Exception { } } + @Test + void playsAvi() throws Exception { + MediaReader reader = new MediaReader(MediaReaderTest.asset(MediaReaderTest.AVI_ASSET)); + + // Opening the player is what fails when a decoder for the file's + // codecs was left out of the FFmpeg build. + try (Playback playback = new Playback(reader)) { + playback.player.play(); + + assertTrue(playback.ended.await(15, TimeUnit.SECONDS), "no end of stream"); + + // The asset holds six frames, all of which have to come out. + assertEquals(6, playback.frames.get()); + assertEquals(1920, playback.width.get()); + assertEquals(1080, playback.height.get()); + + assertTrue(playback.chunks.get() > 0, "no audio"); + assertEquals(2, playback.channels.get()); + } + } + @Test void pacesInRealTime() throws Exception { try (Playback playback = new Playback()) { diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java index b154b073..a8d0b8f3 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java @@ -39,6 +39,13 @@ class MediaReaderTest { /** The committed test asset: 320x240 VP8 at 15 fps, 48 kHz mono Opus. */ private static final String ASSET = "/media-test.webm"; + /** + * The first quarter second of Big Buck Bunny (CC BY 3.0, Blender + * Foundation) as the 1080p AVI it is distributed in, cut without + * re-encoding: MS-MPEG4 v2 video at 24 fps, 48 kHz stereo MP3. + */ + static final String AVI_ASSET = "/media-test.avi"; + @Test void readsVideoInfo() throws Exception { @@ -74,6 +81,22 @@ void readsDuration() throws Exception { } } + @Test + void readsAviInfo() throws Exception { + try (MediaReader reader = new MediaReader(asset(AVI_ASSET))) { + MediaInfo info = reader.getInfo(); + + assertEquals(1920, info.getVideoWidth()); + assertEquals(1080, info.getVideoHeight()); + assertEquals(24.0, info.getFrameRate(), 0.01); + assertEquals("msmpeg4v2", info.getVideoCodec()); + + assertEquals(48000, info.getSampleRate()); + assertEquals(2, info.getChannels()); + assertEquals("mp3", info.getAudioCodec()); + } + } + @Test void missingSourceFails() { IOException e = assertThrows(IOException.class, @@ -102,9 +125,13 @@ void infoAfterCloseFails() throws Exception { } private static Path asset() throws Exception { - URL url = MediaReaderTest.class.getResource(ASSET); + return asset(ASSET); + } + + static Path asset(String name) throws Exception { + URL url = MediaReaderTest.class.getResource(name); - assertNotNull(url, "Test asset " + ASSET + " is missing"); + assertNotNull(url, "Test asset " + name + " is missing"); return Paths.get(url.toURI()); } diff --git a/webrtc-java-media/src/test/resources/media-test.avi b/webrtc-java-media/src/test/resources/media-test.avi new file mode 100644 index 0000000000000000000000000000000000000000..31c03a510ae64c4c6c4e617a9f7873e552b9bb4a GIT binary patch literal 78392 zcmeHQ2V4``_D*8N2&f4tf<_Gl5j$026^#&Sf(jN;ER=|XsDQl#A`6O$6veJId+&-0 zhzcsYcCoDrx?-=mx|aXB_hyC(1QvJyyKmRc`~BW~li|*tx!?WHIo~<=&gkLn;9z3L zX7wE8EOK@3KL8F{qh(>izLNt-TeDcKyfpY9`G5HJSu8dAF-QLY;q_Q7YcAEI;{Q=! z6XEBCiDAL;U)|O-+~J4GNe%eFoH~pK@V0?oscGX%O z*nW#?$Fg$clCr3EQ%wyoiquyX^7N10FlW_!yK}{(Pj}cqe)!U|Wsi~{7Zu%YSk(OE z*U4(*@9KS;IC+)u@YkRzVg%1Wm%0!Hsd@K1v1BJyCHNb*YX4N) z$E4UXy+N_x#tr>eZ<;5QJ=9MbuCZUU8~Z|ux1Oh41}8#m=*y2=9Gf~fj2+T=>1e&o z5FxkjritDo-3GUB(@S(=a%hRe!4kcK0*`YoyEOKR)F|i>v_k8v=KC_|`+D!{_19_H zah~DpnWrTzgTcKg?lU-BPuaZ=pLg%dAF2X*yJ z5Y3-BVu9NBuF;c(&-cGygXUQR6!`kv9%+kk?sT`plFoh~6I`r|M?M`cnCI4hi*3jI zo!k1q`!RD`-_6m-q-*<+?K1a{ck?~-OrOM=Pf6{N{a}|x{`-xS%i6E;pZ;q~@rk0K zH&^E@$xB^ev((S|%9mB^LO1wrAH040lq>5B2R`|^L+TN;@=3S4d=6|hWBi?*x|(*I zZtPxUuwgB4UY*Y==YM#*%V^>T|EOoLAN#Me{(SO>L+9Mn&uqMLUiNh8c)dPb>~77L zTzym4<)-i4=W!X@#lH>9o|QHG{II&e?d`SG`m|MWO!k!R#b$@DZhm4hHtfan-JZ?H z%(&MzYtXN6Oiy2%?ysFNs%hV@me~!uW&h%Lrfa}uTXQGf8Iff_28G@%o3kl~#Y(&B zw(s^d*CKo8$V|sz(ZnGS>dr8oF)gOtY-IlpgJN44Zq0fyJxg=nv&TCv7W+;slfE{4 zd&lf%aQ@yBo!?%xn0)?p;Ev_zUqtcsuXqmGo3P~k&+gM-u4)lA@#v930UstM_^VH8 z*VjzfW9w~;<)=4aO)Qekx8LdF_HoaBk5@*`0t1@aU;U`ob?Tb}35%5w$GRBb`CPZd zJ|6a*c5Si-M@7EwDQF&Mb1%eRoHpX1Bskyc+6~utUe}IVPT@@~5{$MDShG{NnSCz~ z`;vBiam-GmtLb-IOn%TLVYuwatP`y@MA@r@EnDB~;v45?9aENSUo!fNX8XF`Ivxm1 z95*igT8Ctdc4-q!0~I@Qs9cp_tB zqYT!CwY~gJKg_j7JX(u+lrL=*&W#-I{G_k_)VTS2i%iQ#=ncHmajWgK`!hF zdzFo+@SLQ z?@qTXu{akxC@#p!Y~1w1y5}7vef@$4C)%h-P1ByCttE;L7^WvpDZeT*wXtvFHqoWy z))fm&OdM_fc8wWVl(Ke@+x#JME$#F>*sU~q(@x9a!1m}@njMFZ(2Db&nAG6fpx&QAUylwm#=MyPqSdvjSH`aS`sMwd zW8k{Kemyb={^yI&NUiXsKAR373*TU+-*KB7duqS^NpIqWY_5IE0(C3F&r^rkc{Hk* zJBICdB`R>&!E))xmfNG=dB&=F4Ege6WsCMd2Z?WpO=tT4bl1tQ`K)GPV~zXh{!nCF zuxi%XPZ`NCe(EV~;Qp|)v)7T4qxNa940_sPYW&f$&BvHM{j57lQ}0Yu+k>%hG;?nL z(wp7Gx8ah2)ep=kTx_5!d@w`$Ll)Hov*yzS}|dCwa}kWU7^HEw zx63e>OMPdAEC||o#P;d<#$^L7x7a>=9K5mho>Mz#UF#>B@Vb2SjZ+tgzH{(2UD@PB zU}ukBK|KBAy>S7BZMGC`O*#6`aH8|^TkE_h-TpF0th;-iMds1{rC(2aELd_~w)fRa zZ;QiY4D9yY-hSlLL!AfHf9qj+^7EVz=jZj9C&=IS^^12ui+vNHE&X|~;rPh%K}OM+ z^-Z>UXtYc0HKVckNZYO*tdd+q?0@TMNd2dNKa%!-r=+-57rIhs=*G@v&9qW46Wi>2rGH`d-odJDw<6 z*1!Jj3p=8tGu84eg#*tC=pwxo>LCxr5+)w@9JlLB0rad>M zhwH%H5{<-%+{49vuBA>G*W!Nnl#9()_qMQ~VQ!%obTMgPkDEd#&rx%n`aK-yVYSQQ zY>@x>b-8o9r*#du7wV{Tf_ST9^V`Pscm zv%|s*)65OSoTo?Ym->#jP`jI-)6J}F?#jF^oz}Vku!{9HY^RaKz2De@+lO8b9g{xE zv8_eartyO=@9P=&SnuP+p=A$Vbvqf_x@bcOpAjaWfj_2gda#;P|IFGeD<2Oz>~LXS zx`o$VuIY`YO@d#qyttys*nWlOp7z@QBc57akG)#<`fl@p5sU6-c%SWd!vAabTW!-D z;aVq5H|Z=b9e&L6r#d1d?zI2XWE)R{&GGw`QhNS zrKg;~razkf_Qa*fYaQ)h`{}INGJXD|9|P1M^3GqE?RmT>cKS&3$>YY3dZIPtPJ?li z%J#V)`!J-BUF7q+b;1UHUU}AsWgM=S*v@9whRegd-{M*?Uwr9mY_Llgy@lGb!Nmqc zn}2e=f3BeX)jQU)XGcz2JUO|gx6}4fr}O7uAA7p=Q@5YJ7Bvj0r?bbRPjQE6L2~a< zEi3h?b8h1#jvCo^P2ArHwjJhLG<#ED{_S_}8Dkxup0V3d)Vr`q@`2NOQ?Qz2PR@q& z)ApLJF6`Rqa-jZ-UV8pvHpZTXooD>8e%9W*UE@ZxIL(GVcedK>{vmqja?arAKYD96 z@_pN|=%L|<_QeH5y0mR+p6%y#&TH`5pH4>KzxeP`AMU12wwJag&QCh{I&+z>pT~!D zEAHRXNn6J1GR@lmqsP!gkLGD??vl}EzDd&7p(ii&ZrtZ*JtIep;G^p2rjKjq5Uk(J zvZzo0Bwp;zmu5*Ct3{n-yw^DFYrQYirO(O6p{+5C3{dMRxVmq6uYsFK9$&R0rNdRx5eXEkj|xq0r19}<5oaB1yxTf8dGZSvi5^;3Gd3>^^r`l-R5 zyW-LpVL!OX4f$bfdynA{Z^Yc%;%DWQ&gPl3pFO+7AMz|j zeP+%UkK%rHx35_(?)~`Oqbv1BHhGt{ZeFt=#$0N&&uiTcohOsW1bEFpckyxKlSMAh z{H(goIy|`;wkx=?&J*KAzl`H!Ru>;SGQE84r%oZhlLCuBzFWUy$I)~R4ciU3%_nuS z^4z)QW7fytns<71EJJqq-N>%&t@XMyJD~WjoGn&e|z6 zyf$yviRT7dr+$lW-zL{?fAhFUMJ}yoTW)xFaaO^`y?VwmsS&Rhip>Xd+U&X|=o!4^ zrpv7>S%-se&j}p5wd0{1Yfdy9z`@gRhTeL7(R$y;#L`VB>HX}tr5Sj1@i25N zEghXUxNXr7i49zq+3hcG?LEcn?cM>;&9-iOH|uc%tB#cGJ!4Lh!z#xUkJN{jI0kng z)O>>zKc&B**yL$Pk@U9>^L9w1c4x2BTY90e&l~IfrL$(F;^F^ennaUIjyJS6$d#<8|#<2`l`Yro}4$J4gkTkk1veNgx0 z6rGL6YS|h|vv&Hbxh|h{Um7M|sMfV@Pj9#JFOQ8mnU!9CVcOvV$-sb*#)t9Vi!^O>t{TYTI{mkVP^NUun)iGfs^4o z9$w?Tomsfy?V)GVdEI?{;(l!U$%ET+Rq&~ECz5yp8Clt{OE=BCb!g+ZnAkQRW{a+m z3U1|-GAr)VV~tk+XV0b>4D;GH;LMT*vHOm;m2NsW+xrJMbvMuCyGzHvOWU@2$dj_( zr9rz~)~}x3VMt!CK8v-C)%fy_OAf=V&-4hBlr65)VZe^qw0`PwXU*61_lCUo7&o)v zj(|7LlXqHTa?fvf?|}}MsXK%s$@GsqboR55pK6SpVx^$bp7a^ zjozO|E!j4KwQ5qc??g+Sn(>f5mmlg1D6hHZ!-(-m!CJQTW9% zamK5L6`D02VlZy@g^K}grffX_z@H%yEZt#~!3 zuEEOor^{!LKfUfs0V~=)`N8s^%S#TpI_B1&^r)@OV#6lScki)H`|4ImYp(pl(#-|?Y&LLgsYcd48A|)mDz0z zqc$4jLoHjI6gHT!b<6CgFE~?lyZhDCv(wspX7Jg^9d!;LSvoZPqSmR{j;p(;zRUk* zXZva8*JkMq_j@;{so8GhvQA8RIw{2Ft6%F6o7%W0KC>J7 zWTVEWR=00;IK0KZf$1u*CkUf?-U--vA-8Q#-QIBeakrM`C#ENrEb>~(w8^nyYiw(rt$ zI*}VXOh4=OI{#$Kv%Ondsn06Na6FQ~XuD02#rkobr8Ao5KW*36)bnen)-zI7CjqOaHdJVAM9Hswj`D4S6+)-WD?>l}w*?alH@*zhj zygom+^U`y3hh-0TaZmA_w$f|ruA@iyUfQO`YHQ*-cJjRItMhmtkKgA;jNftK#LWD; zdNV(jT^(`Y-~f{)(_&NGIwZ_JF=$rnK~_6n^lSw&ewn7*XQ^3oa$n0P_u7uU(|YE( z&It?EZ;sYHbo8xNzwNtcyJfz-IN{Kh0d-{U2L+$$B5Pb{dHgcdy6nJ*=hHrqoj-AY zQ*ZAcuPqO(I+y*vwdbawa$|C7EIM!AO6fNFZJo?q_n=zNBWc;7<$sGB);h|CQl8x+-E*6{X6sy zS-g30pzXu)I)itL-E2GNP3=@#isG`%j#Yt%hUQciCp?v|@pG za>i#r@#}~y{(U`@hexdHKj`-MD}fKL4_dp_o|l}Lu;O5so+HOt|C~Eb{fD&!tWMec z2Nn(6dqD6u#?52r>N8GPgC{(n6CyOPTX=b}@%Y9&e~b^X>lm1uak|@y(FOsc6~%3= zS1vnrCDVERP|0q#|dqlX&4!>t)Fd6I{4Oa-No*=@`s+;eS7Dy zYX`2+NG-1)Q)hjTkEJY@b1#=ZhXjW2`@9+Z$$I*AU2d#NeV$L~fh5j>-`q|#cN=G- zcPFB|uW(z!qY&*yO+d6Dfzny9{VU^#}J7x->oqqX zZ`Z@TjYi*VzRA%$t$Wt*_JWt%lubb&siMf7SPHvF6qGjpjM^+D$SJ=ddK~ zt@Do#SRmrMWG%kcOVW4Yk4D!EN)N2xIVzxj!Hvz`w8l?h6Y?G;G|e zLAhx6Oi$PT_8-&YuZXoZTXxT~owr`+;iY8`n_dqx9ls&%_PZt*PBa_e-^%1#o&3wY z%|kj{c6q*a_u<~=MlB`vlDj`jf784Cm-i0_yc(hD>ld^2`fAzr#r>a}(2M+^{lt7P?QKptb54H)tG_ry^F}qF^WUslCJ2_TKn|Z`nAlj-j1FKb|(( zI`Um~>x0MIw0)R0Z1B_I=byPYJ#`C?zswG3VD8ZRu(3hIsqeNp#g2>~dh~Sp`mOeP z={6gNyWSe$I^g8oQH|S$TBrPJOn~ z!#kg{x>_1X7M1vKdwE7U?L**{t}ZRNHOTC6AuGdTWPk0|rV~dF-+ismsN~nhZULIZ z&NaR|<=wl34=?Wuu;{to$#d>&^Q$p!oMqa}IyCyB->4w>R_>RETU?%Uags|$qYe?~ zSpi>0?AbCX_;PHf&8Gu-8E4zfICy0A`8xyj`!Cducf6l9>X%`uo07izE&+Cwt}kB;5H#me~B-E5zu?wr$4(+&mS^0?_@Coc0Zh+Q6;;5>4L^Qz>2 zZQJ%9nH1ur{dJ=U>r+nfE8?rygW(&*95zBpy(sPfDKMI(>5a&FUY z-hghqyUf~lyW^{|(dVph56BcQ=pHpRjun#A<-yITyCN^&8Cg24{~Oo1wDLD@4L1KA zGb5u+bL!e2=j**}aU#&=g?dcGplJhK9tCf|+^Nab&Ec5?4rEW*-AV7>7E@0hQ@dRm z>(Zh-c=qC_iu;+G*y_0MAU^~$+HW6{psLAN)Vv~Yesy1$cuscG80SA)4cgz<@mtBvBp2*6**yfzxFF&pIo&ID^FPH5j;#wZMcC7C0 zrHO0GKDM~F{q?U)`dAD<^3ZE;@YLg9wU@6}59t4K%Iir>MjY#V;b7|WTj^&SKG?A2 zg;lE~^}oidf7ts>C>$N0`f}D&omsyW^)G$OlCU@nlG-oUX~J^I?0CCaZ)2Y|QvzAL zcpNa;9^Jtlnwf8gpyPb~k538yEqe4K1z<&?{l^6IA zPP%HyDg+b|Pb^Tz+S-Ii^Zxp^^3k?<1i;dp~%rNLYTnuivO($N9|p%0@sije>vOcMNp zDZ`Bc+}{Q8`)EMeMyv`@Q?zHrUzP2dnhTnVV3>d&)y(=ktiw@Es>vDvMFj}JK{8Dm zV@giY>~usnP8cYtJO|hX5Uzb+h|*4uLJNk@IK<oVl>}-*MdR=GkRYQtJjxj|{-!AC=x9P=sYpuN0Z`ZjxYVS4&xZ`cDT+S; zgz>jpl8eH$3dI#Ci<*M}$XO}|eUGAr1l7OJQkYuWy^?QZYQ+B-rT&QJ5En;H6;U^# z^7!#rPa`AHe|Lv;!G}a=%tXa-LU~q|TdY;djnd!*HY&2KK%`}0t*sx_5G4|c8e*h8 zDm=dlPfSWhNvU$0e^eqvBro|7WX*q9;y@~DDmwmAI)VyHm)FCIS5%BZe~Gq?smL#7 ze|q`ppX19hj*x4UgpnvB>YVbYG(a(G@nX5QhhMdW!$6E?Oi!4IuO~B4{i1tQe#d{+ zS(4-sJqd1;QOU#rFpORxOf;)lR)b$KPKsrKObw$JixZ`ehA|4uYUFxBrIhPmuV5)g z{!Y2Epu#$(-)fx%O(jSHWgY$!U-JJwOWz{7amnYe%IScWN^m5)t=696n`B-MH>|;c zs=WohUQ^jviQR^NgL%bNc9N8BJN+=V|4hBI>a%iN*t1gITciB984BPZjhh>C_~H2U=qy9-wH}e=%IrQBTjJ@ zq!Yd$-AuW&sP_WUbRY@b`r=nKm^eRa7GF7L_2u=!(n)qfJb8?gJpkPJt1`;b|C#U*!=h zGePm;Yt8vTdYL=kOQi%x<@8|8zY=25gM*71-V9nQ>5ZsQ<#x^pEsQW;WsHnwe>qUK z2>n&qh|vO281qj@Q3jhBhZr?tq6}`*xKe~>=osjmwsjn zRGEXnk-y4D#=uVDkm)81XC?Bq<+=4ITp}qcl2Rk7 zz1@JVQ|Q6m)Q*-gk}7!?x1t|>s2t(iKbOM56)yu!ftlYp`Be-prXny<_aeRnd$HQ^ zsoE|TWj>X*R@)^N-I#KKF)bKQ0y%@h)%I59si^5yGD8e@LVwhAD5CxClw!bH`JUhY z)QS#6T8U-V_D79{U<#N(KQeSN4J0-$V)2Af_{1L^do4Xst>>oNas6#Q)ZkTV)5d_p zqmnp*f6;%VNbtu|t@T|0ZJ1PNNxw5&)p{BjVFB_&4F3HEA?S}AaP8(!SyZasm4DFK zlwF3&M@Lu;EI>&IDg&jJgMS`z;5q-i;ry8%6e9uvM4}f?#DGXtgj6;Kf9AG-HAnwP z&x={9=pE<~jxGN?9rs^K1=?Fu=+cn>LI3<8yNvRMw6|o;mI_ij8{*bNYGRQDl84TQ za2}>;@c(~F^#90-G3fFCdI(&LVdh{&N6=7GR>=s7R9;1g_1er4)zA0bRtZMHN)pS- z!iamJK`1Jc5H?D~oRX3DtScz26I$TsUa{?oyxWkT$Ude2&r?T#`xG+xDXU;B_4@T< z@}?%KM(8DS{d56TBsvpxN)j5UCpx4j2=IcKx(yrSig_zCptiDDS8PW6+eh{{SVavP zBmyEG@e_6dkUGvS%*b0l34%yJaiSX9gvCv>;mTw@>$~9h)Jnn=;&SY)4W$82f?TmU znicOfSeU>K;R{klj%;yUA>|tso$q(UVX_rm$*kc`&4`JV5d)ss9V|A^*Iyhf7Mn=o zo$N?jVIobBl*HTX>6$e(7x79LAIKNE7xEl@fRXid%~}g1&CJ-mT$Y#{9&g9_UuPnJ zl{6*+2D~CTK?+Z@Gn`tP#jk7wMHx@vP?9Kc6p7;^>gt6@`RO9hS)d^X^X9g7G;Bcj zlbgcFE+ochMPg`bs0pB(vw8Ot#0ipkz8rsMkBrX$y@xP1;1-uxYC@sKc->uzPa|bp zbyGv$-30gJEC=q~xSU?XXs*mokZa(+3~VxmgyLGOKdn^lFfgXn&Cb2FJM>pZQ z*SHJ+Uu*saV8OW}M@`sdM#YzE^qxEY-j_%gqP0@SgGeJs}^$kfVNm$NCYvCfqPxM0icjIA@MI*TvLuoVBw36#sso2{=OhfbrTX-N=xD z{{U?W9pyab&5hG;hC(xs2VY-F;DH8m6zL12taQPjYRW=lVjvJja~EFc%!Cg98fi z92Ej^75}hsd;|Z3r(zPV<5<6=QAmD7zK}u4R9KyH0 zm*8Fi9EE5W6K@AG-%9Ql)x~Bk6utF%rF9nLJx~mV7%2M^&~ogeSgz^AF9rBhR*r@QQlKAQP!3HHIY_<}KGIgD8KDaXW(U43J&_;Jt|yF=Ys+Y0MO_qw z`GQCvgcC+GS6ytNlg^id29I+VAjg*o@rE15gS5th8zD^C1?mdoPNYMLNSdA~iHp^W zcW#dP<${+qPzowS716i|_{;Yu+UrABr*4hEu$PNIhvuLO!Pm^3NOu;98ljmyIA}%) zIY7+B?9zU9`dXUW75I({Mn)d3=3BbI3J87lalXxW`DaM+D#5y5b|S2x@Hq z)Ee^NYv|m+%zqCtk0vt{EOa0Lh`_6NGZ)F{SBd#FxoPFxHQS zK&2Z_f;aN1WaZKhb5f6E${UB0Mo54hU`I7dir^eRk&qKtPE@&$R|nb<H5C@{R&YY2+hfdP>D; zNgB%o(cf)});_#^@DgaY%pe_8U~Dui&WU4f$SdHBqb1<#%^*Yq$yjm6K~3;-!^P43 zI6IDs4=)$&gdZ2nG1W&6qWE#z9BX}EAxdH45ERAP1Sk!_p}8S1UxaK57Z&oP&>^&y zAvMU&fjSMT9RPWFcIM3QV^S|J&>Ngd1&xTnf8Lig!Aj<`9Dr%yk&!S!L=tg*as;u! zlL)KMu-=p)(*p+?ckEh9<6PX1O^`KQEDET_HL`f#o1{m{&47`P&YWDMH$A z2NsN=NaV_RNH@TfA^(9!D%D5B<9a>ZAMtTd;#~gf7-0$Q|fIu`d9^ zss!vBN-%&Gz$Qb(lwn&1|DTAa0|qbBM`uAj>MiTt<-WXq*oWcSz-vH1ZzfGBic#^v2JL7X8(;foC< zam$e1mqvj%K=w(rGYfJHFl%rDcx;4fkV$aBfO$}PNs!w;5KumN$U{iJ_dloFx6s`| z#s(1vRtHoQVJqPXau88KtOh{T;~`oma8Vsv0umO+22?9R^da>Vib){}h>Q9WLNzYJ zL4vd~5U>DxK#v1##Vgx*G1f^Rd>oi1hE3N9fk}cec29r`3($@NRKUiun!|?xi5?%F1&1jE z5ljr5ioo)07P>?pgoKoTh|?pJ{Ec&YoQ1@U8cLf08-NtBu@l6gLnxF03Xq4fk*_4~ zjiOW`%aM&ZN8OD+1plW%KYSVSe{yu`8YA4W1k4#l=}MwOZsh;2v!*=WJuGr`Io6uI z0<#pDX*${w!)SE15wtJ#1$YAK?w50<2h(-_e_u`)T0KCFoN;ZV}m#XVWL8u zB=FIKoeYnS{<@>}-6(g6%lgQ^aSUfz3}T`ls(sULv4Y(Hsu;r<6Y^wWj3|V_Avi#= z>}4Q`^ofSSVFAvQ&m>qO?pq-5waQ98m8!oIvH zgKWMonib@F5&!UKC@}v@A_WdIMXM(3GRR5OKKS2zh|~)XH7M&q0|Ft#5u`%(Pajk_ zBp`W|LdRhV8bIv|Gz|nR8oy9E6cn2BYQEgfktC1&jcQ3ptADXUS9$cgl$4Ysg_0c( z$Rj9XplU=8FvbB48hbqK2hjS5JYA%sb~+*-u~>{1S!sb?BgXRjDKln&5ByVZMTCl= zDV?C2gN6y}Q5VTTAAO-2A}EMXLtLJQys?(XM-?3u0R|F(mt<7H6>4YHFwr4YNSy>Z zK9KkT?I8R@?E*y?gah!BD?e1|0aJ}2=#E?@9my$P(l_{iNl5`B<6%;wQUned04c~A zKaQNL{3sbzC0J0lAea+VPi$zd-#7sM12TXg_0LQwQWEbCt0kHL{hWma2eO)QRF}o8 zKL8p4gVP>hCr+yMSW&V_D81oOgGP)@9b$1^>_aK$fkz}Yekjo5IwEc0APOjF`kfzw z_F*Iy@?v{9Ih&s#){+qE4mJ4-MMyzH(T*8eO5_>{6O^f)sEe?i|K89QNl>c7MHO;b zv=hKW1{x-$KoF7j(ODA-tR0y`Hk2TaVw4FX1(Gum=h)yKPtgvSkXLT#Xz`CZQH(kJ z&JOL+l0hlPkqw)SctFC0a5Q**-d%ha>X0H{t_Wq7TIg(U0-_dPxR<*~2=5A_5yVo( zA`N!D-yCWTs-jG`0se#CDcTU@gDOMvXah+j)F@&6SR)}}N$PG9LQxc2&V!vJDDcol zqDWLlM|HQkbbSZ*b=nJ{IcWoRBBlf)`^0OobFxtvIcu8a(AYW{#K!t1_ zt!YDY0k{`fJFPSW|3KubOm-A1Qvxs|gb}RcHAl8_yq=zJv;lGsCSsDw(=|z8UiIwEHHD;<7G0Gdm@MJT{+QSJ zYv%*4E+laMQ02#wi?VB2?S`T+fCZ)<1Iq+NyaQ+Pk^)L9odkJ4h^WyO5GsZhkrcK- zfO7?xYtOOni5#&>&bNe~IPl4u|GF3t5gH<_6UQ7D#uCs97hVO=HxV15Lu)4-g%A#8 zF&e6Fz)mpn0wvB&KIxiq;5E%)MLQgn6Nz5{CZoKGgpzVzlGkpysBrI7;S{QP&A*mi zf8xgA2J!L(#2eUxf!DpEUdurijQ9xwDM~K5(Y_-&yOjJ3THEPMlyul$wb7IRGSBDV&w( zH+V`J4?{Nc&|x43Jw7~-`~*1HWgOH?1Z-qXtL-9~gCp0Nl-KV@RaBal;{s2H=@uamEdWZ?twz*);eoSjN>$h^H;CrvQi+VpjmGV6dxB1`5bP z&NaD{t9U6+0FnqI*j(6^5ffK?qs9He9#W918sccW*XkWJn!1{#RSj1%1sp@mDIC_8 zkCY9-enSyIF5J#UiZ)`Xp>+xLYE^Uqlu~J7YX9Fr>h|r6CWD6!AZ2>|{*RZB7 zw-CPE-=Y0vh%P>0+X^k+3hoJVp-& z%-6@8fS9r9B2FjKcHCW7F?hJ>L$+ zn<#N4)+0zDhobE##%QY}jJX0O#-PqZs3t`qF6=-<^uo#s1tj80VMgy^z!6^sLjEoY zWOwJGmfX0w0)a3p3T-EXwI$F>%BhF=2L2e8Taf(yE0jBMfs%Oi%)qpQ#bBybgi-}( zQW(oEtFk;pgTM3Scu`v_?+L;i_3qlr4m z-zP@D9O}c93m|_di^sbsuPRv~su^}D-7txc7Rt$Q=A?yam=tGDB zKZzA75j3b8Ck4`?&jPUG^?-_Is8VOTxIi7$lS6&Eb1aZE0w@TZgsa{|ZvL>v3$}a` zAy&nLV=5j=ra&PaH5HYkA`yuQpqR$^8|7k1v;}`gNUGSL_6f+sDo9Wg1>R7ur-1=z zSEUrvZdl8eqSY~k3ErqyQ3+BEsX#SI`e4Lnh>?*J6wYAqXgj^5ArCf5gS)slkKB`N zfg!iYFN45g^=ojbKd1+CBVczTF@cKe5eo9m3JX{c#+3n|Vz5xOY8oF*HE>(dn6x0g zgn0ye$EWG0a0`5b2-nI}R{<%C=Hm(DSue`W1&TGSY#|ff{3dR zjt%N>;O1oVtp&`7t(X9Xvks8RO^33iQ&y=)kSJdo&s0)O4uZK*2}kMQk^N6R8^jr$jIvNA~$}VrJ;)p(5w^TNzhat zyRL5nQ?))KU8Z$Vx} zGedGM)u+J(+qWHB(Ve=8GFtlUdR zflW#{OekEY=zSIS4>Bm`EtIvz)lwura9FG4?rNZ0wf#~dniw-ok%2s5Gcf)GhfwSR zUx)Xrf^tv7Jun;*-d#;ExXNogsD6x{1SBP3Y53C%hHQ9o7hd(t^HDSpTKiWuns1;I zkg+0j`etTV_W3bNClKr#KM_^)AgOt^^8voJ>NG+|ULUDnGx=pCpQdJ?QT!rW4-*$w zzgbv6%UJ~)3pflfd@GTCv_z~01C>)0v=B&iE)uVpqTj9xl=oY){D)B?P7)|8L#Y0q zQ7f4bqBJQS(I$a#dP#>oxDxn5N3qdMyijg}kHcYf)$mL}vV`7gQD&&$c2-e6f_=lA zTsYOC^;v}+{_}UM=z$S*l;w*xI8;TSfUFcJzw)}dLX;#y#{?@KwD$+SG7g`RV>!6O z!UHmH{2?mEumueii#&hS&e^DN2X7+CSEV4@;k%(apTa|all3bP@L<^l6`TYqGbPFw zR1+$x3WB3ZE)3zj7qI^g@64h?J1l}@MT#6C_OT#Np+O$pyT))`xqkl5coO|4mA?y! zM~Zgk)c(zm3Nm5rb42MT@I^-WKEQk%kr1hCmZPC@1vPPv8ww-v2V&=R*&Uro&{2#B zq~d~R8F7l5THq7}B0y0VA!Jbt5Om#$;6=uTOKot71iHKjhHO_PN6l(qv2ArQqga>9 zj6SqLBCKSNqH>(UG~<9mF_8&F_{R6bLlb;TVBZsb|4IvnZhV)Id{TXRB#CAN$oL&P z8Aw|6VPOms`i72~4Nn-RWYHQ&2uKU73kyj5zzPFj4EsEs?4S$;juwItaZ$LmgO|7R zqtKQv2n$MFs-imsmkQh9a$xd_zhgwS->rYZPUKkZi)50pxM6$WMesqK_MZ-6{82)cOKNw8}+aMYme%aTJ|6_Da}K zz$ozD!0BMJ6W1<^LmQH-OMF6{fQe#$?nwz!Z}1LHK6!^GLI|5B6Vcp62mRt=D-2PnG3OMJ-R!`P8^vjAS$kO`guEiyEHNh)7D>cFv>Bm$Rh2u~f8xt1Qzjk&*x`d-PKE8;VC!ssvX;wq(KWb}>iius zUhoSzt);$igsXYz{RR1D@{h`+ZzSU>uEY!+?$BlcE*UKO!WL}GQ(TDv?^VfHPzqEe zg|#dM3wrt88oy0%gaRU(V?Ih)U}7OYgPn&n5p&13wAX7F@<)c?T3&x;U2_A3{`3!RkcQ25XR3Rh3 zBnt5s@W){i<_OMeqPWAHt_tZ5iq#JpssEv<;eC#E~*bBwx-|0J74;9R3CdAOmfH zZHR*=@{AS;LRB*G>mwx3#R3k*9_-`C@dN8VI7FLSE2b-|V)22T#qk5?6ZRI61vfqj zU$mza`(_erxMFC4T`%p#D9$JbqZ(E+7|#Hmu@TiO0KuM&!BfT|y|73}WBf4nu<|el z14HqoV&>tH9|iA|Fmey#rugv`phUqyN*fEhh7I3i0%Zgxbl83gvXRmsxV{Fo!kUNEY=b(MXpqC8MA~|ugW;B~C5u3W30&y@bjA&eGc=EOt z&RemFLu5975oV^imM45kX>%l^$fHxRwDP z0&8kYZOZ8*kD`oVL~dLaQ#p_o-lT$nXoB#IhRXd0#)!bB&;SP|x@rNhDFHA5Z>5&- z;9B?w8e)^i@XsoMpn_LTkVirIlW(Izu@<3-DF(5b=fGp@MWJdg0*MXR17DE?!Z2fA zRjl*K*@oRH+0?;7DZ4a6PlUJ)TRmWh7El*m1~(*{9X`tggf`=n4ItQp7%^WF8Da-Y z03}Xa37`rYi%)`esfoLMu(T;C010Pfn7#!9YwoEk{uL*NP+;Kd>n0dL$UKWa3XYTs zU9dnh2?8kWoPvEBg{bNXq(#CKsqS3-HEVl$ZVCn#vT_3VH8IIFFNV?(8c@Eg^0+k?oOZ4GoU2qDh-iF!&t4MNzLpHWe#jFvT zo_xm?xr~cGWlP8id{?9$Sb@CXDinohz)BJ?5ffJdP614cumkwcCY>D@rPEpsfIq2Z zXAr}AO7V?w+5)4S*)#|@O Date: Sat, 26 Sep 2026 00:22:11 +0200 Subject: [PATCH 15/33] fix: keep video fed when a container stores audio far ahead of it One thread decodes both streams in the order the container stores them, and it blocked whenever the audio queue held its second of audio. Coarse interleaving, and a frame-threaded decoder that returns each frame several packets after it was fed, can put video more than that behind audio in the file. The video queue then ran dry and frames arrived late and in bursts, which WebRTC's encoder answers by dropping all but the last frame of each: an H.264 MOV played at 5 to 10 fps although every frame was decoded. Audio may now run past its capacity, up to ten seconds, while the video queue is below half full. The test asset stores all of its audio ahead of its video, in Matroska, which is read strictly in file order. --- .../src/main/cpp/include/media/MediaPacer.h | 14 ++++++ .../src/main/cpp/src/media/MediaPacer.cpp | 12 +++++- .../webrtc/media/ffmpeg/MediaPlayerTest.java | 40 ++++++++++++++++++ .../webrtc/media/ffmpeg/MediaReaderTest.java | 6 +++ .../src/test/resources/media-test-coarse.mkv | Bin 0 -> 130646 bytes 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 webrtc-java-media/src/test/resources/media-test-coarse.mkv diff --git a/webrtc-java-media/src/main/cpp/include/media/MediaPacer.h b/webrtc-java-media/src/main/cpp/include/media/MediaPacer.h index 37773ee2..546bd1ec 100644 --- a/webrtc-java-media/src/main/cpp/include/media/MediaPacer.h +++ b/webrtc-java-media/src/main/cpp/include/media/MediaPacer.h @@ -58,6 +58,20 @@ namespace ffmpeg static constexpr size_t kVideoCapacity = 60; static constexpr size_t kAudioCapacity = 100; + // How far audio may run past its capacity while video is short. + // + // One thread decodes both, in the order the container stores + // them, so a full audio queue holds up video as well. Containers + // interleave coarsely, and a frame-threaded video decoder hands + // out each frame several packets after it was fed, so video can + // lag audio in the file by a second or more. Blocking on audio + // then leaves the video queue running dry, and its frames arrive + // late and all at once, which WebRTC's encoder answers by dropping + // all but the last of each burst. Letting audio grow while video + // is below half its capacity is what keeps video fed; this bounds + // how much, at ten seconds, for a source that has no video at all. + static constexpr size_t kAudioOverflowCapacity = 1000; + // The sources may be 0, in which case media of that kind is // dropped rather than delivered. MediaPacer(const webrtc_java_api * api, void * video_source, diff --git a/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp b/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp index 9d7fdba7..bcf131f4 100644 --- a/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp +++ b/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp @@ -144,7 +144,14 @@ namespace ffmpeg std::unique_lock lock(mutex_); audio_space_.wait(lock, [this] { - return !running_ || audio_queue_.size() < kAudioCapacity; + if (!running_ || audio_queue_.size() < kAudioCapacity) { + return true; + } + + // Full, but blocking now would keep video from being decoded + // while it is running short. + return audio_queue_.size() < kAudioOverflowCapacity + && video_queue_.size() < kVideoCapacity / 2; }); if (!running_) { @@ -263,6 +270,9 @@ namespace ffmpeg position_us_ = item.timestamp_us; video_space_.notify_one(); + // A shorter video queue may be what lets audio past its + // capacity. + audio_space_.notify_one(); lock.unlock(); DeliverVideo(std::move(item), due); diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java index a3ac7269..54f80b63 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java @@ -25,6 +25,9 @@ import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -149,6 +152,41 @@ void playsAvi() throws Exception { } } + @Test + void coarseInterleavingKeepsVideoEven() throws Exception { + // All three seconds of audio are stored ahead of the video, so the + // video can only be decoded once the audio has been read. A player + // that stops reading while the audio queue is full gets to the video + // late and delivers it in bursts, which WebRTC's encoder answers by + // dropping all but the last frame of each. + MediaReader reader = new MediaReader( + MediaReaderTest.asset(MediaReaderTest.COARSE_ASSET)); + + try (Playback playback = new Playback(reader)) { + playback.player.play(); + + assertTrue(playback.ended.await(15, TimeUnit.SECONDS), "no end of stream"); + + List times; + synchronized (playback.frameTimes) { + times = new ArrayList<>(playback.frameTimes); + } + + assertEquals(75, times.size()); + + // 25 fps is a frame every 40 ms. A frame on the heels of another + // is one that was due earlier and arrived late. + int bunched = 0; + for (int i = 1; i < times.size(); i++) { + if (times.get(i) - times.get(i - 1) < TimeUnit.MILLISECONDS.toNanos(10)) { + bunched++; + } + } + + assertTrue(bunched <= 3, bunched + " frames arrived in a burst"); + } + } + @Test void pacesInRealTime() throws Exception { try (Playback playback = new Playback()) { @@ -284,8 +322,10 @@ private final class Playback implements AutoCloseable { final AtomicInteger channels = new AtomicInteger(); final AtomicInteger framesPerChunk = new AtomicInteger(); final AtomicReference error = new AtomicReference<>(); + final List frameTimes = Collections.synchronizedList(new ArrayList<>()); private final VideoTrackSink videoSink = frame -> { + frameTimes.add(System.nanoTime()); frames.incrementAndGet(); width.set(frame.buffer.getWidth()); height.set(frame.buffer.getHeight()); diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java index a8d0b8f3..e204fd77 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java @@ -46,6 +46,12 @@ class MediaReaderTest { */ static final String AVI_ASSET = "/media-test.avi"; + /** + * Three seconds of 160x120 MJPEG at 25 fps and 8 kHz mono PCM in + * Matroska, with all of the audio stored ahead of all of the video. + */ + static final String COARSE_ASSET = "/media-test-coarse.mkv"; + @Test void readsVideoInfo() throws Exception { diff --git a/webrtc-java-media/src/test/resources/media-test-coarse.mkv b/webrtc-java-media/src/test/resources/media-test-coarse.mkv new file mode 100644 index 0000000000000000000000000000000000000000..598f39c5ea0a4eaa43f8d02dac6de27750dc148e GIT binary patch literal 130646 zcmeI53s_WT9*4iVF~eY7WS|j&vCNusTfCK*QU(;kwiL__YgH08D6&vAk(3!U)9A6N zRzbHFq}|jk5HEqINS2qPP24Qdwqih-5wRu^1X0eOnZPrrV$f}kZw^0uPM*UVW;!ze zcfR-SJdb{1-@PRwwqMgiU+mZui9#=OyA7SY($L1vT}9}Mj$Ot4*^3rFo0Kp+bam%R zY3QoXU9tLML!9kEbfwN`(zYi$PE5P2yr5%O;t{HQ$k1`%xny4K!bNlLeron}vj;4i zm$WE4q^mO%Ug|ib&}~VyU4ZYjyo8Rg)#P>T%q#EM5s8uxU)^1Kb??p2J<@sc@<(DG zdLS|^d|-9wqom^4$4F(z164aZzawW~=U#H>t37k|)x_MZi94T1B>v^A-4qfNIsT!b zNt34qJveEavEvzU-_x<9&>hs-t}VOswM4OVJ9j-bG*kyUsyqi zfsl2|bKW(6I|IK7jP3u9%HY;UrZ=Ce-&B)kBu4F-H|sJRmFd|LU{OEi6giEn!fx0N*evNxT;?3?J)zU$i)RubFhN_av z4OR6fwPnzyYN@*idyaifQ9yyZ*z<%7`+Yx-K0CF#tg_u$RGawyeJvL8cV;>VyHN_PALr^RGeeOqn^ZbWs#DNk0GQ1AUJ+JI+FrVF5eWr51QCpjPe$vHe z(UX1V`c(8M8hv1_-yPmtU1f~n!XxK4of&AHZ0uj-Q=ib(+kPH)Z$_;FKO788PY#RS zn=#S=l7VDB`N?AUW=uBVV+J4lJ-H0Cdo$)3uqMMAxhL1ic5lXG5Euwdj|!#To6!P+ zf$jIG?Q?HNks1Pn6w{+=%u1_DEgz__R(Fc26@1ja=Nfq}qKA}}uT5EuvyB?9A;0fB+QP$Doc zMGzPW3@rjv34wvYP$DpL1q22HLy5r1hd^K;Fq8<4d^!XM0z-+w$PEw}2n;0xBi{;v zfxyrrFl7)J2n;O((*l8kz)&JEiUAN92n;0xqlkdOKwu~l7)2rk1_DEgz$mgHFc26@ z1V&K;fq}r#A~0qM3q7 z*BuZT2n;O(Qvrd2z)&JEN&-5Ex1XMwtwOfxu8AFv=VV z3n6Jrr;_24My^qq<{U-OmUY_1+t)JT0S3Pv# zpaAWd;bZR~J$%$iUBryZx(U%?BS%HfkBW|ojf;;P_t1i+iF1=@%!`|2eG-vZ-@a-e zb&#K5(45dwp>wYL7gIvK6eOACNJO_1v6o2VC1O4%8uq_aCbIn^9lu0kiB#qySGX$O z*az(KBw~?7B9=;IGO3h(^m6t*k$TDc-ZC`A#XD-Y{MH4kVQH^zRtyX+_|RwS3CnH6 zV;8P)Rr=nn?&mk?_B*t9-ZkQ$dq<8MJ!brbu!-RjlOA|@+MgeZp8n`#bLP%_A};>P zr;-*eer`$f(v+1ita@?vnwQdFf8(#~)@NpI*qHm)KmPglmaW^~*|}@?p7#rj_U=3M z(Z`3696k2Qr~m%qe4qPNYEXuUMsm0jC~t@xj=?COPa*L9lok%-tIp2Uj;6O%bNooN4`TYgCH{+RR6 zexDj_anIkrbkp)MRrKzKt5i`snS(8GCHt@=f!j$&TEcy~{Cqt0uX zQ*ZtB#*wC`5iN!r!~N^ja+cxFTYV_Ip^0HXlE|s;Ir+oAvYz*QCHwC4CiY9_RqpZM znzEy2dZaU?ro3Iwke%gYXBMRtpWe7FVaBHmYSyht*S@z#sm(QXwT;mz^`+&PB@B5Y zZ^d4Q*c}ze4Pi)fW@$tC=J3k!;HrbIiy4x}korBxqsQ}(c4ac;gQKR_U9T4&Ih3x= z`(i+Dx|VdYNj6)yTt1*L32uJMJUH8Kzc$I^;!Dd8-jAGFM$@8k zb_dUo*0=3X)&Fhu;AKy<7(G9%t1IituLpA&lBJPIU^F5}Mvxn**EndH4+Llk|hjusjBtIm79J}3oZ<{8#tnQ=5%Er12evNg* zvI`rojQ1{Mhq?Kc$S$B^gUN4x0!L4N1J6k~8W_?@GH3uAI5`dWajS-R7x*k+J927y zLE@_;-c33gol|bVQaHT&v$1=^rmQb-WQZYQjDNv_&r{O1`45^Lema|%-N}%k7>mc1 zjeG0t4vKbpTwJ>B;E06q>hO>m46$4G{OoWoyCT^iZfYH#S6E_u#kcRbIGS+YGHVm> z=w)^lv?Q~%apLaO5sQ3|x8%3a)K}`;?zZ1gKBaH_I930~m~+een6~DoUz&5=;RQNa zklo1dFy71jlrf5Q3fys3<(26$R7?8N0;}x+28dcDgq5a1A)*0`NOI52e&M- zrjJU12p|#&h>$;Yy!0VfV7M{d!WnMl4`<0A+!}*TAL1dv5HKVh3?YA9Xa3;b^&y@P z8h{1@q5<-U)8r3sS;D3du>l|ghy(0>K~8^bNE;YR*&hWx>;F|6sM3>X52 zgoPpGkL$=E{PfWR8h{1@qXF`VQ{)eBS;D3d$pC-|AQBjedU5{Xr4LC2h8x2zyx~Uv zpgn(ZYYaAhND_e|U`TivLjJhM{J~2fk}S{wG!P&SkUyx;AKbFUnm$SZB7jJMAnK+0 zgP%Uk7;X%=FozrYgXa9ftufg2A@u}?fFWUG2>Ih0@&_+{NJoJNpn*VXfc!yq{@|7+ zZ2FKs4iEuE0tHbo%pbh;Azg#v#&8RFxRF0-%^%zv!z5*Px8 zgpDEO4+r^!mp)|4paEzga2gh6UFvOs*|FSGP*#6+NTFnoATT>%0XfG=o>$JJWS&fI% zwK-x3oBQfR*$qt$Aq+XSJtt3Zf0V!Bv*2$BHC%qp9M$4wIvEwVe5h%aD$4fvQ_FtG z-#J`JYSLsD-~zZ1J}!QlDO6E2-wtsA1{*YHt(>)--TpYgd7OUpmoL^WZBj+eI`OG0 zDvUVVe9ZTlzBJ!cRr}o>)1`pv&uqz#3iCPdzqz}Yy8-9r++Q-QN^?!QL`#L*UQwsyZn`;BH71Rp%O!oga=wob7756MfhKlsB( zSH6u2`j_8{mJ`QWIGmpAh$lDdoc<~t*eu|p0~`Pc=Y~UmNbdfa^Ur>t8fVN@j87NTtYbf`-&>>9<{G-%#%PrK((=m^hCGqCVlPAN zj*8=kFeEv%v>|+Rcx8BS)xp-q3`t{1{T_CGz&qNN$&e3@np$_gUU=kCx;F2N0lDc~ z(#0m(Y}s=8fW9QS`6=_@Y`gv1B#(T8y^FzCvdy*fLKaSmQzPC*iTvqqd zVr6691;56+VcCTZSH^pnX>-$?Uy1Aj8a9~x<|lCU>hN&UME ze3q{rIkmhX@zoLUCY_AVDYsuK9A5p|*gauW)|WT3|FaZ=Kyi(JqgROP3uSkq}-T9x{U=cFUfh9j;|pB>Tfnt;6#QOKh+B z_Wc$|6V6*^ZQ>oh%&vl#WR^Bg+?_gNkA8y_!aZdhludL_&Udg`uyotTtz`V*m{##Ra6!J%R zpZmY(d2WrtrVsfLUdw4Fo^~0>K~8^bM(;YR*&rn^75HHI~PlmSD)kT5WW{NW&Rba|f}cMq5$P2K_; zfCd7g0rH1auf zaEAQBtufg2;pzzt0Yk#V5c0=$fue_Uh!;HQrY&;T?LAPtZ|sLvnV zvV=_^N&*l8L;?g+FU=pk^r6&ZxG~(q9B$+fn)3&@#$eNjG7=a9hJ=YB2`Ge~G!7WRy=_3aq0*C|(qF$Ik`03*qh8x2z+~G$4pf!JRYYc1pumD5A zkZ>`C{LxMR;H3{YHD~}D2$%-QAJpa#Zdt;n4>uh^1P}=rM7=D3@Y07{Jcb*?E$rb& d{-7~`aBB=UeYj-+L%@)*F@*f#Ab+sw!pP literal 0 HcmV?d00001 From 1567535854c4dafdba4a58b79b1789756574a548 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 00:22:18 +0200 Subject: [PATCH 16/33] feat: add a media file player example MediaFilePlayerExample plays a media file through two peer connections in the same process, without a signaling server, and shows the received video in a Swing window while the received audio plays on the speakers. Start and Stop set up and tear down the whole session. Beside the video, a panel shows what the file contains, where playback is, and what the receiver gets for each track, read from its statistics: codec, resolution, frame rate, bitrate, packet loss, jitter and audio level. The video sender's maximum bitrate is raised above WebRTC's default of about 2 Mbit/s, which would otherwise hold a 1080p file to a fraction of its resolution. --- docs/guide/examples.md | 21 + .../examples/MediaFilePlayerExample.java | 1084 +++++++++++++++++ .../src/main/java/module-info.java | 1 + 3 files changed, 1106 insertions(+) create mode 100644 webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java diff --git a/docs/guide/examples.md b/docs/guide/examples.md index a39d646f..2802c623 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -73,6 +73,27 @@ mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.MediaFileExample" -D" ``` ::: +## Media File Player + +The [`MediaFilePlayerExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java) is a Swing application that sends a media file from one peer connection to another in the same process, then shows the received video and plays the received audio in sync. Start and Stop buttons set up and tear down the whole session. + +**Key features demonstrated:** +- Connecting two peer connections in one application, without a signaling server +- Using one factory with a dummy audio layer to send the file, and another with the platform audio layer to play what is received +- Keeping audio and video in sync by sending both tracks in the same media stream +- Raising the video sender's `maxBitrate` above WebRTC's default, so a high-resolution file is not held to a fraction of its size +- Rendering received video frames in a Swing component with `VideoBufferConverter` +- Showing the file's format, the playback position, and live receive metrics (codec, resolution, frame rate, bitrate, packet loss, jitter, audio level) read from `getStats()` +- Releasing senders, peer connections, tracks and the source in order on Stop + +::: info +Like the [Media File](#media-file) example, this one needs the `webrtc-java-media` module. The file argument is optional; a file can also be chosen from the window. + +```bash +mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.MediaFilePlayerExample" -D"exec.args=movie.mp4" +``` +::: + ## Web Client The [`WebClientExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/WebClientExample.java) demonstrates how to combine WebSocket signaling with WebRTC peer connections for real-time communication between web and Java clients. diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java new file mode 100644 index 00000000..62c5f878 --- /dev/null +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java @@ -0,0 +1,1084 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc.examples; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.Timer; + +import dev.onvoid.webrtc.CreateSessionDescriptionObserver; +import dev.onvoid.webrtc.PeerConnectionFactory; +import dev.onvoid.webrtc.PeerConnectionObserver; +import dev.onvoid.webrtc.RTCAnswerOptions; +import dev.onvoid.webrtc.RTCConfiguration; +import dev.onvoid.webrtc.RTCIceCandidate; +import dev.onvoid.webrtc.RTCOfferOptions; +import dev.onvoid.webrtc.RTCPeerConnection; +import dev.onvoid.webrtc.RTCPeerConnectionState; +import dev.onvoid.webrtc.RTCRtpReceiver; +import dev.onvoid.webrtc.RTCRtpEncodingParameters; +import dev.onvoid.webrtc.RTCRtpSendParameters; +import dev.onvoid.webrtc.RTCRtpSender; +import dev.onvoid.webrtc.RTCRtpTransceiver; +import dev.onvoid.webrtc.RTCSessionDescription; +import dev.onvoid.webrtc.RTCStats; +import dev.onvoid.webrtc.RTCStatsReport; +import dev.onvoid.webrtc.RTCStatsType; +import dev.onvoid.webrtc.SetSessionDescriptionObserver; +import dev.onvoid.webrtc.media.FourCC; +import dev.onvoid.webrtc.media.MediaStreamTrack; +import dev.onvoid.webrtc.media.audio.AudioDeviceModule; +import dev.onvoid.webrtc.media.audio.AudioLayer; +import dev.onvoid.webrtc.media.audio.AudioTrack; +import dev.onvoid.webrtc.media.ffmpeg.MediaFileSource; +import dev.onvoid.webrtc.media.ffmpeg.MediaInfo; +import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerListener; +import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerState; +import dev.onvoid.webrtc.media.video.VideoBufferConverter; +import dev.onvoid.webrtc.media.video.VideoFrame; +import dev.onvoid.webrtc.media.video.VideoFrameBuffer; +import dev.onvoid.webrtc.media.video.VideoTrack; +import dev.onvoid.webrtc.media.video.VideoTrackSink; + +/** + * Desktop UI counterpart to {@link MediaFileExample}: plays a media file + * through a pair of peer connections that live in the same application, and + * shows the received video while the received audio plays on the speakers. + *

+ * This example shows how to: + *

    + *
  • Send a MediaFileSource over one peer connection
  • + *
  • Connect it to a second, receiving peer connection without a + * signaling server, by handing descriptions and candidates across + * directly
  • + *
  • Render received video frames in a Swing component
  • + *
  • Play received audio through the default playout device
  • + *
  • Show what the file contains, where playback is, and what arrives + * at the receiver, read from its statistics
  • + *
  • Start and stop a session repeatedly, releasing everything in + * order
  • + *
+ *

+ * The two sides use two factories. The sending factory is fed pushed audio, + * so it gets a dummy audio layer. The receiving factory gets the platform's + * audio layer, whose playout is what makes the received audio audible. + *

+ * Audio and video stay in sync because both tracks are sent in the same + * media stream: the receiver lines them up with each other before handing + * them on, so the video sink only has to draw each frame as it arrives. + *

+ * Run it with an optional media file to preselect: + *

+ * java dev.onvoid.webrtc.examples.MediaFilePlayerExample movie.mp4
+ * 
+ * + * @author Alex Andres + */ +public class MediaFilePlayerExample { + + private static final Logger LOG = Logger.getLogger(MediaFilePlayerExample.class.getName()); + + /** Both tracks go in this stream, which is what ties them together for lip sync. */ + private static final List STREAM_IDS = List.of("media-file"); + + /** + * The most the video may be sent with. Without a limit of its own, WebRTC + * holds video to about 2 Mbit/s, which fits a call but leaves a 1080p file + * at a fraction of its resolution. The two peers here share a machine, so + * there is room for more; the bandwidth estimate still decides how much of + * it is actually used. + */ + private static final int MAX_VIDEO_BITRATE = 10_000_000; + + private final AudioDeviceModule senderAudioModule; + private final PeerConnectionFactory senderFactory; + + private final AudioDeviceModule receiverAudioModule; + private final PeerConnectionFactory receiverFactory; + + /** Setting up and tearing down a session blocks, so it stays off the EDT. */ + private final ExecutorService worker = Executors.newSingleThreadExecutor(r -> { + Thread thread = new Thread(r, "media-session"); + thread.setDaemon(true); + return thread; + }); + + private final JFrame frame = new JFrame("Media File Player"); + private final JTextField fileField = new JTextField(32); + private final JButton browseButton = new JButton("Browse..."); + private final JButton startButton = new JButton("Start"); + private final JButton stopButton = new JButton("Stop"); + private final JCheckBox loopBox = new JCheckBox("Loop"); + private final JLabel statusLabel = new JLabel("Idle"); + private final JProgressBar timeBar = new JProgressBar(0, 1000); + private final VideoPanel videoPanel = new VideoPanel(); + private final MetricsPanel metricsPanel = new MetricsPanel(); + private final Timer statusTimer = new Timer(250, e -> updateStatus()); + private final Timer statsTimer = new Timer(1000, e -> requestStats()); + + /** Only touched from the worker thread and read on the EDT for status. */ + private volatile Session session; + + + public static void main(String[] args) { + String file = args.length > 0 ? args[0] : ""; + + SwingUtilities.invokeLater(() -> new MediaFilePlayerExample().show(file)); + } + + private MediaFilePlayerExample() { + senderAudioModule = new AudioDeviceModule(AudioLayer.kDummyAudio); + senderFactory = new PeerConnectionFactory(senderAudioModule); + + receiverAudioModule = new AudioDeviceModule(); + receiverFactory = new PeerConnectionFactory(receiverAudioModule); + } + + private void show(String file) { + fileField.setText(file); + + browseButton.addActionListener(e -> chooseFile()); + startButton.addActionListener(e -> start()); + stopButton.addActionListener(e -> stop()); + loopBox.addActionListener(e -> { + Session current = session; + if (current != null) { + current.source.setLooping(loopBox.isSelected()); + } + }); + + JPanel controls = new JPanel(new FlowLayout(FlowLayout.LEFT)); + controls.add(fileField); + controls.add(browseButton); + controls.add(startButton); + controls.add(stopButton); + controls.add(loopBox); + + timeBar.setStringPainted(true); + timeBar.setString(""); + + JPanel statusBar = new JPanel(new BorderLayout(12, 0)); + statusBar.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8)); + statusBar.add(statusLabel, BorderLayout.WEST); + statusBar.add(timeBar, BorderLayout.CENTER); + + frame.setLayout(new BorderLayout()); + frame.add(controls, BorderLayout.NORTH); + frame.add(videoPanel, BorderLayout.CENTER); + frame.add(metricsPanel, BorderLayout.EAST); + frame.add(statusBar, BorderLayout.SOUTH); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + + @Override + public void windowClosing(WindowEvent e) { + exit(); + } + }); + + setRunning(false); + + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + private void chooseFile() { + JFileChooser chooser = new JFileChooser(); + + String current = fileField.getText().trim(); + if (!current.isEmpty()) { + chooser.setSelectedFile(new File(current)); + } + + if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { + fileField.setText(chooser.getSelectedFile().getAbsolutePath()); + } + } + + private void start() { + String file = fileField.getText().trim(); + if (file.isEmpty()) { + statusLabel.setText("Choose a media file first."); + return; + } + + boolean looping = loopBox.isSelected(); + + setRunning(true); + statusLabel.setText("Connecting..."); + metricsPanel.clear(""); + + worker.execute(() -> { + try { + Session started = new Session(file, looping); + session = started; + + SwingUtilities.invokeLater(() -> metricsPanel.showMedia(file, started.info)); + } + catch (Exception e) { + LOG.log(Level.SEVERE, "Could not start playback", e); + + SwingUtilities.invokeLater(() -> { + setRunning(false); + statusLabel.setText("Failed: " + e.getMessage()); + }); + } + }); + } + + private void stop() { + startButton.setEnabled(false); + stopButton.setEnabled(false); + + worker.execute(() -> { + closeSession(); + + SwingUtilities.invokeLater(() -> { + setRunning(false); + videoPanel.clear(); + statusLabel.setText("Stopped"); + timeBar.setValue(0); + timeBar.setString(""); + + // What the file contains stays up, what was playing does not. + metricsPanel.clear("play."); + metricsPanel.clear("video."); + metricsPanel.clear("audio."); + }); + }); + } + + private void exit() { + statusTimer.stop(); + statsTimer.stop(); + frame.dispose(); + + // Queued behind anything still running, so a session being set up + // is closed rather than left behind. + worker.execute(() -> { + closeSession(); + + receiverFactory.dispose(); + receiverAudioModule.dispose(); + senderFactory.dispose(); + senderAudioModule.dispose(); + + System.exit(0); + }); + } + + private void closeSession() { + Session current = session; + session = null; + + if (current != null) { + current.close(); + } + } + + private void setRunning(boolean running) { + startButton.setEnabled(!running); + stopButton.setEnabled(running); + fileField.setEnabled(!running); + browseButton.setEnabled(!running); + + if (running) { + statusTimer.start(); + statsTimer.start(); + } + else { + statusTimer.stop(); + statsTimer.stop(); + } + } + + private void updateStatus() { + Session current = session; + if (current == null) { + return; + } + + MediaPlayerState state = current.source.getState(); + long positionUs = current.source.getPositionUs(); + long durationUs = current.info.getDurationUs(); + + // Live streams and some containers have no duration to measure against. + String time = durationUs > 0 + ? formatTime(positionUs) + " / " + formatTime(durationUs) + : formatTime(positionUs); + + statusLabel.setText(state.toString()); + timeBar.setValue(durationUs > 0 ? (int) Math.min(1000, positionUs * 1000 / durationUs) : 0); + timeBar.setString(time); + + metricsPanel.set("play.state", state.toString()); + metricsPanel.set("play.position", time); + metricsPanel.set("play.connection", current.connectionState.toString()); + } + + /** + * Asks the current session for its statistics. This goes through the + * worker, which is where sessions are closed, so a request can never + * reach a peer connection that is being closed at the same time. + */ + private void requestStats() { + worker.execute(() -> { + Session current = session; + if (current != null) { + current.requestStats(); + } + }); + } + + private static String formatTime(long us) { + long tenths = us / 100_000; + long s = tenths / 10; + + if (s >= 3600) { + return String.format("%d:%02d:%02d.%d", s / 3600, s / 60 % 60, s % 60, tenths % 10); + } + return String.format("%d:%02d.%d", s / 60, s % 60, tenths % 10); + } + + private static String formatBitrate(Double bitsPerSecond) { + if (bitsPerSecond == null) { + return null; + } + if (bitsPerSecond >= 1_000_000) { + return String.format("%.2f Mbit/s", bitsPerSecond / 1_000_000); + } + return String.format("%.0f kbit/s", bitsPerSecond / 1_000); + } + + /** + * Formats a codec entry of a stats report, such as {@code VP8} or + * {@code opus, 48000 Hz, 2 ch}. + */ + private static String formatCodec(RTCStats codec) { + if (codec == null) { + return null; + } + + Map attributes = codec.getAttributes(); + String mimeType = String.valueOf(attributes.get("mimeType")); + String name = mimeType.substring(mimeType.indexOf('/') + 1); + + if (!mimeType.startsWith("audio/")) { + return name; + } + + long channels = count(attributes, "channels"); + + return String.format("%s, %d Hz, %d ch", name, count(attributes, "clockRate"), + channels > 0 ? channels : 1); + } + + private static String formatLoss(Map attributes) { + long lost = count(attributes, "packetsLost"); + long total = lost + count(attributes, "packetsReceived"); + + if (total <= 0) { + return null; + } + return String.format("%d of %d (%.1f %%)", lost, total, lost * 100.0 / total); + } + + private static String format(String pattern, Double value) { + return value != null ? String.format(pattern, value) : null; + } + + /** + * Reads a counter from stats attributes. Counters come as Integer, Long or + * BigInteger depending on their native type, and are absent until there is + * something to count. + */ + private static long count(Map attributes, String name) { + Object value = attributes.get(name); + return value instanceof Number ? ((Number) value).longValue() : 0; + } + + private static Double decimal(Map attributes, String name) { + Object value = attributes.get(name); + return value instanceof Number ? ((Number) value).doubleValue() : null; + } + + /** + * One run of the file, from Start to Stop: the source, the sending and + * receiving peer connections, and what connects them. + */ + private class Session { + + final MediaFileSource source; + final MediaInfo info; + + final Peer sender; + final Peer receiver; + + private final VideoTrack videoTrack; + private final AudioTrack audioTrack; + private RTCRtpSender videoSender; + private final List rtpSenders = new ArrayList<>(); + + volatile RTCPeerConnectionState connectionState = RTCPeerConnectionState.NEW; + + /** The received video track, kept to detach the sink from on close. */ + private volatile VideoTrack remoteVideoTrack; + + /** Turn the byte counters of successive stats reports into bitrates. */ + private final RateMeter videoRate = new RateMeter(); + private final RateMeter audioRate = new RateMeter(); + + + Session(String file, boolean looping) throws Exception { + source = new MediaFileSource(file); + info = source.getInfo(); + + sender = new Peer(senderFactory, "sender"); + receiver = new Peer(receiverFactory, "receiver"); + + sender.remote = receiver; + receiver.remote = sender; + + // A source with no video has no video source, and likewise for + // audio, so each track is only made if there is something to feed + // it. + if (source.getVideoSource() != null) { + videoTrack = senderFactory.createVideoTrack("video", source.getVideoSource()); + videoSender = sender.peerConnection.addTrack(videoTrack, STREAM_IDS); + rtpSenders.add(videoSender); + } + else { + videoTrack = null; + } + + if (source.getAudioSource() != null) { + audioTrack = senderFactory.createAudioTrack("audio", source.getAudioSource()); + rtpSenders.add(sender.peerConnection.addTrack(audioTrack, STREAM_IDS)); + } + else { + audioTrack = null; + } + + source.setLooping(looping); + source.setListener(new MediaPlayerListener() { + + @Override + public void onStateChanged(MediaPlayerState state) { + LOG.info("Player state: " + state); + } + + @Override + public void onEndOfStream() { + stopIfCurrent(); + } + + @Override + public void onError(String message) { + LOG.warning("Playback failed: " + message); + stopIfCurrent(); + } + }); + + try { + // The offer and answer go straight across, where an application + // talking to a real remote peer would send them over signaling. + RTCSessionDescription offer = sender.createOffer(); + receiver.setRemoteDescription(offer); + + RTCSessionDescription answer = receiver.createAnswer(); + sender.setRemoteDescription(answer); + + if (videoSender != null) { + raiseMaxBitrate(videoSender, MAX_VIDEO_BITRATE); + } + } + catch (Exception e) { + close(); + throw e; + } + + // Frames are sent in real time from the moment playback starts, + // and whatever goes out before the connection is up is dropped. + // Starting right away keeps the example short; an application + // that must not lose the first second would wait for CONNECTED. + source.play(); + } + + /** + * Lets a sender use up to the given bitrate. Parameters are read, + * changed and written back as a whole, and only have encodings to + * change once negotiation has set the sender up. + */ + private void raiseMaxBitrate(RTCRtpSender rtpSender, int bitrate) { + RTCRtpSendParameters parameters = rtpSender.getParameters(); + + for (RTCRtpEncodingParameters encoding : parameters.encodings) { + encoding.maxBitrate = bitrate; + } + + rtpSender.setParameters(parameters); + } + + /** + * Stops from a player callback. Closing here would wait on the + * player's own thread, so it goes through the UI like the Stop button, + * and only if the user has not already moved on to another session. + */ + private void stopIfCurrent() { + SwingUtilities.invokeLater(() -> { + if (session == this) { + stop(); + } + }); + } + + /** + * Collects the receiving side's statistics and shows them once they + * arrive, unless the session has ended in the meantime. + */ + void requestStats() { + receiver.peerConnection.getStats(report -> { + Map values = readStats(report); + + SwingUtilities.invokeLater(() -> { + if (session == this) { + metricsPanel.setAll(values); + } + }); + }); + } + + /** + * Picks the inbound RTP entries out of a stats report, one per + * received track, and turns them into the rows of the metrics panel. + */ + private Map readStats(RTCStatsReport report) { + Map all = report.getStats(); + Map values = new HashMap<>(); + + for (RTCStats stats : all.values()) { + if (stats.getType() != RTCStatsType.INBOUND_RTP) { + continue; + } + + Map attributes = stats.getAttributes(); + Object codecId = attributes.get("codecId"); + RTCStats codec = codecId != null ? all.get(codecId) : null; + Double jitter = decimal(attributes, "jitter"); + Double jitterMs = jitter != null ? jitter * 1000 : null; + + if ("video".equals(attributes.get("kind"))) { + long width = count(attributes, "frameWidth"); + long height = count(attributes, "frameHeight"); + + values.put("video.codec", formatCodec(codec)); + values.put("video.resolution", width > 0 ? width + "x" + height : null); + values.put("video.fps", format("%.1f fps", decimal(attributes, "framesPerSecond"))); + values.put("video.bitrate", formatBitrate(videoRate.update(stats))); + values.put("video.frames", String.format("%d decoded, %d dropped", + count(attributes, "framesDecoded"), count(attributes, "framesDropped"))); + values.put("video.lost", formatLoss(attributes)); + values.put("video.jitter", format("%.1f ms", jitterMs)); + } + else if ("audio".equals(attributes.get("kind"))) { + Double level = decimal(attributes, "audioLevel"); + long samples = count(attributes, "totalSamplesReceived"); + long concealed = count(attributes, "concealedSamples"); + + values.put("audio.codec", formatCodec(codec)); + values.put("audio.bitrate", formatBitrate(audioRate.update(stats))); + values.put("audio.level", level == null ? null + : level > 0 ? String.format("%.1f dBFS", 20 * Math.log10(level)) + : "silent"); + values.put("audio.concealed", samples > 0 + ? String.format("%.1f %%", concealed * 100.0 / samples) : null); + values.put("audio.lost", formatLoss(attributes)); + values.put("audio.jitter", format("%.1f ms", jitterMs)); + } + } + + return values; + } + + void onRemoteTrack(MediaStreamTrack track) { + if (track instanceof VideoTrack) { + remoteVideoTrack = (VideoTrack) track; + remoteVideoTrack.addSink(videoPanel); + } + // A received audio track needs nothing: the receiving factory's + // audio device module mixes it into playout on its own. + } + + void close() { + // Stop drawing before anything goes away underneath the sink. + VideoTrack remoteVideo = remoteVideoTrack; + if (remoteVideo != null) { + remoteVideo.removeSink(videoPanel); + } + + // Senders are not owned by the peer connection. Releasing them + // before it closes lets go of the tracks they carry. + for (RTCRtpSender rtpSender : rtpSenders) { + rtpSender.dispose(); + } + + sender.close(); + receiver.close(); + + // The tracks go before the source, which disposes of the media + // sources they were made from. + if (videoTrack != null) { + videoTrack.dispose(); + } + if (audioTrack != null) { + audioTrack.dispose(); + } + + source.close(); + } + + + /** + * One end of the in-process connection. Candidates found here are + * handed to the other end, held back until it has a remote + * description to match them against. + */ + private class Peer implements PeerConnectionObserver { + + final RTCPeerConnection peerConnection; + + private final String name; + + private final List pendingCandidates = new ArrayList<>(); + + private boolean hasRemoteDescription; + + Peer remote; + + + Peer(PeerConnectionFactory factory, String name) { + this.name = name; + this.peerConnection = factory.createPeerConnection(new RTCConfiguration(), this); + } + + RTCSessionDescription createOffer() throws Exception { + CompletableFuture offer = new CompletableFuture<>(); + peerConnection.createOffer(new RTCOfferOptions(), createObserver(offer)); + + return setLocalDescription(offer.get()); + } + + RTCSessionDescription createAnswer() throws Exception { + CompletableFuture answer = new CompletableFuture<>(); + peerConnection.createAnswer(new RTCAnswerOptions(), createObserver(answer)); + + return setLocalDescription(answer.get()); + } + + RTCSessionDescription setLocalDescription(RTCSessionDescription description) + throws Exception { + CompletableFuture done = new CompletableFuture<>(); + peerConnection.setLocalDescription(description, setObserver(done)); + done.get(); + + return description; + } + + void setRemoteDescription(RTCSessionDescription description) throws Exception { + CompletableFuture done = new CompletableFuture<>(); + peerConnection.setRemoteDescription(description, setObserver(done)); + done.get(); + + List candidates; + synchronized (this) { + hasRemoteDescription = true; + candidates = new ArrayList<>(pendingCandidates); + pendingCandidates.clear(); + } + + candidates.forEach(peerConnection::addIceCandidate); + } + + void addRemoteCandidate(RTCIceCandidate candidate) { + synchronized (this) { + if (!hasRemoteDescription) { + pendingCandidates.add(candidate); + return; + } + } + + peerConnection.addIceCandidate(candidate); + } + + void close() { + peerConnection.close(); + } + + @Override + public void onIceCandidate(RTCIceCandidate candidate) { + remote.addRemoteCandidate(candidate); + } + + @Override + public void onConnectionChange(RTCPeerConnectionState state) { + LOG.info(name + " connection state: " + state); + + if (this == receiver) { + connectionState = state; + } + } + + @Override + public void onTrack(RTCRtpTransceiver transceiver) { + RTCRtpReceiver rtpReceiver = transceiver.getReceiver(); + + onRemoteTrack(rtpReceiver.getTrack()); + + // The receiver and transceiver are query results the + // application owns. The track itself is unaffected and keeps + // delivering frames to its sink. + rtpReceiver.dispose(); + transceiver.dispose(); + } + + private CreateSessionDescriptionObserver createObserver( + CompletableFuture future) { + return new CreateSessionDescriptionObserver() { + + @Override + public void onSuccess(RTCSessionDescription description) { + future.complete(description); + } + + @Override + public void onFailure(String error) { + future.completeExceptionally(new Exception(name + ": " + error)); + } + }; + } + + private SetSessionDescriptionObserver setObserver(CompletableFuture future) { + return new SetSessionDescriptionObserver() { + + @Override + public void onSuccess() { + future.complete(null); + } + + @Override + public void onFailure(String error) { + future.completeExceptionally(new Exception(name + ": " + error)); + } + }; + } + } + } + + + + /** + * Turns a byte counter that only grows into a bitrate, measured over the + * time between two successive readings of it. + */ + private static class RateMeter { + + private long lastBytes; + + private long lastTimestampUs; + + + /** + * Takes the next reading from inbound RTP stats. + * + * @return The bitrate in bits per second since the last reading, or + * {@code null} if there is nothing to compare against yet. + */ + Double update(RTCStats stats) { + long bytes = count(stats.getAttributes(), "bytesReceived"); + long timestampUs = stats.getTimestamp(); + Double rate = null; + + if (lastTimestampUs > 0 && timestampUs > lastTimestampUs && bytes >= lastBytes) { + rate = (bytes - lastBytes) * 8 * 1_000_000.0 / (timestampUs - lastTimestampUs); + } + + lastBytes = bytes; + lastTimestampUs = timestampUs; + + return rate; + } + } + + + + /** + * A column of named values, grouped into sections: what the file + * contains, where playback is, and what the receiver gets for each track. + * Each value is addressed by a key such as {@code video.fps}, and shows a + * dash while there is nothing to show. + */ + private static class MetricsPanel extends JPanel { + + private static final String NONE = "-"; + + private final Map values = new LinkedHashMap<>(); + + private int row; + + + MetricsPanel() { + super(new GridBagLayout()); + + setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12)); + // Fixed, so the video does not shift as the values change width. + setPreferredSize(new Dimension(320, 0)); + + addSection("Media"); + addRow("media.file", "File"); + addRow("media.duration", "Duration"); + addRow("media.video", "Video"); + addRow("media.audio", "Audio"); + + addSection("Playback"); + addRow("play.state", "State"); + addRow("play.position", "Position"); + addRow("play.connection", "Connection"); + + addSection("Received video"); + addRow("video.codec", "Codec"); + addRow("video.resolution", "Resolution"); + addRow("video.fps", "Frame rate"); + addRow("video.bitrate", "Bitrate"); + addRow("video.frames", "Frames"); + addRow("video.lost", "Packets lost"); + addRow("video.jitter", "Jitter"); + + addSection("Received audio"); + addRow("audio.codec", "Codec"); + addRow("audio.bitrate", "Bitrate"); + addRow("audio.level", "Level"); + addRow("audio.concealed", "Concealed"); + addRow("audio.lost", "Packets lost"); + addRow("audio.jitter", "Jitter"); + + // Takes up the remaining height, keeping the rows at the top. + GridBagConstraints filler = new GridBagConstraints(); + filler.gridy = row; + filler.weighty = 1; + add(new JPanel(), filler); + } + + void showMedia(String file, MediaInfo info) { + set("media.file", new File(file).getName()); + values.get("media.file").setToolTipText(file); + set("media.duration", info.getDurationUs() > 0 + ? formatTime(info.getDurationUs()) : "unknown"); + set("media.video", info.hasVideo() + ? String.format("%s, %dx%d, %.2f fps", info.getVideoCodec(), + info.getVideoWidth(), info.getVideoHeight(), info.getFrameRate()) + : "none"); + set("media.audio", info.hasAudio() + ? String.format("%s, %d Hz, %d ch", info.getAudioCodec(), + info.getSampleRate(), info.getChannels()) + : "none"); + } + + void set(String key, String text) { + values.get(key).setText(text != null ? text : NONE); + } + + void setAll(Map newValues) { + newValues.forEach(this::set); + } + + /** + * Resets every value whose key starts with the prefix, all of them + * for an empty one. + */ + void clear(String prefix) { + values.forEach((key, label) -> { + if (key.startsWith(prefix)) { + label.setText(NONE); + label.setToolTipText(null); + } + }); + } + + private void addSection(String title) { + JLabel label = new JLabel(title); + label.setFont(label.getFont().deriveFont(Font.BOLD)); + + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = row; + c.gridwidth = 2; + c.anchor = GridBagConstraints.WEST; + c.insets = new Insets(row == 0 ? 0 : 12, 0, 4, 0); + + add(label, c); + row++; + } + + private void addRow(String key, String name) { + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = row; + c.anchor = GridBagConstraints.WEST; + c.insets = new Insets(1, 0, 1, 12); + + add(new JLabel(name), c); + + JLabel value = new JLabel(NONE); + + c = new GridBagConstraints(); + c.gridx = 1; + c.gridy = row; + c.weightx = 1; + c.fill = GridBagConstraints.HORIZONTAL; + c.insets = new Insets(1, 0, 1, 0); + + add(value, c); + values.put(key, value); + row++; + } + } + + + + /** + * Draws the most recent received video frame, scaled to fit while keeping + * its aspect ratio. + *

+ * Frames arrive on a WebRTC thread already timed to match the audio, so + * they are converted there and only the drawing is left to the EDT. + */ + private static class VideoPanel extends JPanel implements VideoTrackSink { + + private BufferedImage image; + + private ByteBuffer pixelBuffer; + + + VideoPanel() { + setBackground(Color.BLACK); + setPreferredSize(new Dimension(960, 540)); + } + + @Override + public void onVideoFrame(VideoFrame frame) { + VideoFrameBuffer buffer = frame.buffer; + int width = buffer.getWidth(); + int height = buffer.getHeight(); + + try { + synchronized (this) { + if (image == null || image.getWidth() != width || image.getHeight() != height) { + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + pixelBuffer = ByteBuffer.allocateDirect(width * height * 4) + .order(ByteOrder.LITTLE_ENDIAN); + } + + // libyuv's ARGB is B, G, R, A in memory, which read as + // little-endian ints is exactly what TYPE_INT_RGB holds. + pixelBuffer.clear(); + VideoBufferConverter.convertFromI420(buffer, pixelBuffer, FourCC.ARGB); + pixelBuffer.rewind(); + + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + pixelBuffer.asIntBuffer().get(pixels); + } + } + catch (Exception e) { + LOG.log(Level.WARNING, "Could not convert a video frame", e); + } + finally { + frame.release(); + } + + repaint(); + } + + void clear() { + synchronized (this) { + image = null; + pixelBuffer = null; + } + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + synchronized (this) { + if (image == null) { + return; + } + + double scale = Math.min((double) getWidth() / image.getWidth(), + (double) getHeight() / image.getHeight()); + int w = (int) (image.getWidth() * scale); + int h = (int) (image.getHeight() * scale); + int x = (getWidth() - w) / 2; + int y = (getHeight() - h) / 2; + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(image, x, y, w, h, null); + } + } + } +} diff --git a/webrtc-examples/src/main/java/module-info.java b/webrtc-examples/src/main/java/module-info.java index 28989e1f..8f970f32 100644 --- a/webrtc-examples/src/main/java/module-info.java +++ b/webrtc-examples/src/main/java/module-info.java @@ -1,6 +1,7 @@ module webrtc.java.examples { requires com.fasterxml.jackson.databind; + requires java.desktop; requires java.logging; requires java.net.http; requires org.eclipse.jetty.server; From 5c25ca00d9168e216faf53baea228db18aa8e1a2 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 00:32:42 +0200 Subject: [PATCH 17/33] feat: give the media file player the platform's look and feel The window used Swing's cross-platform default. It now switches to the system look and feel before the first component is created, and falls back to the default with a warning if that is not available. --- .../examples/MediaFilePlayerExample.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java index 62c5f878..5dd8160d 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java @@ -56,6 +56,7 @@ import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.Timer; +import javax.swing.UIManager; import dev.onvoid.webrtc.CreateSessionDescriptionObserver; import dev.onvoid.webrtc.PeerConnectionFactory; @@ -174,7 +175,26 @@ public class MediaFilePlayerExample { public static void main(String[] args) { String file = args.length > 0 ? args[0] : ""; - SwingUtilities.invokeLater(() -> new MediaFilePlayerExample().show(file)); + SwingUtilities.invokeLater(() -> { + usePlatformLookAndFeel(); + + new MediaFilePlayerExample().show(file); + }); + } + + /** + * Makes the window look like the platform's own applications rather than + * Swing's cross-platform default. It has to happen before the first + * component is created, which only picks up the look it was made with. + */ + private static void usePlatformLookAndFeel() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception e) { + // The default look works everywhere, so this is only cosmetic. + LOG.log(Level.WARNING, "Could not use the platform look and feel", e); + } } private MediaFilePlayerExample() { From 0071e2549a5c3c3f84f598fdd3c6d3922a2e4924 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 12:00:14 +0200 Subject: [PATCH 18/33] test: judge starved video by its lateness, and report why adaptation failed coarseInterleavingKeepsVideoEven counted frames that arrived close together, which also catches a runner that is merely slow for a moment: both macOS builds saw 17 and 18 such frames. A player that starves its video delivers it seconds late instead, about two seconds with this asset. The test now measures how late the worst frame is against the first audio chunk, which shares its timeline, and fails above a second, printing when each frame arrived. lowBitrateScalesDown failed on the armv7 runner with nothing to say why. It now waits up to 30 seconds for slow runners, and its failure message carries the sender's last outbound stats: whether it encoded at all, with which encoder, and what limited it. --- .../webrtc/media/ffmpeg/MediaPlayerTest.java | 33 +++++++++---- .../CustomVideoSourceAdaptationTests.java | 49 ++++++++++++++----- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java index 54f80b63..84155f6b 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java @@ -31,6 +31,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import dev.onvoid.webrtc.PeerConnectionFactory; @@ -157,8 +158,8 @@ void coarseInterleavingKeepsVideoEven() throws Exception { // All three seconds of audio are stored ahead of the video, so the // video can only be decoded once the audio has been read. A player // that stops reading while the audio queue is full gets to the video - // late and delivers it in bursts, which WebRTC's encoder answers by - // dropping all but the last frame of each. + // seconds late and delivers what is overdue in a burst that WebRTC's + // encoder answers by dropping all but the last frame of it. MediaReader reader = new MediaReader( MediaReaderTest.asset(MediaReaderTest.COARSE_ASSET)); @@ -174,16 +175,26 @@ void coarseInterleavingKeepsVideoEven() throws Exception { assertEquals(75, times.size()); - // 25 fps is a frame every 40 ms. A frame on the heels of another - // is one that was due earlier and arrived late. - int bunched = 0; - for (int i = 1; i < times.size(); i++) { - if (times.get(i) - times.get(i - 1) < TimeUnit.MILLISECONDS.toNanos(10)) { - bunched++; - } + // Frame i and the first audio chunk share a timeline, so frame i + // is due I frame intervals after that chunk arrived. How late the + // worst frame is tells a player that starved its video, seconds + // behind, from a busy machine that was merely slow for a moment, + // which a count of closely spaced frames cannot. + long start = Math.min(playback.firstChunkNs.get(), times.get(0)); + long frameNs = TimeUnit.MILLISECONDS.toNanos(40); + long maxLateNs = 0; + StringBuilder offsets = new StringBuilder(); + + for (int i = 0; i < times.size(); i++) { + long offsetNs = times.get(i) - start; + + maxLateNs = Math.max(maxLateNs, offsetNs - i * frameNs); + offsets.append(i == 0 ? "" : " ").append(offsetNs / 1_000_000); } - assertTrue(bunched <= 3, bunched + " frames arrived in a burst"); + assertTrue(maxLateNs < TimeUnit.SECONDS.toNanos(1), + "video was up to " + maxLateNs / 1_000_000 + " ms late; frames arrived at " + + offsets + " ms after the first audio"); } } @@ -323,6 +334,7 @@ private final class Playback implements AutoCloseable { final AtomicInteger framesPerChunk = new AtomicInteger(); final AtomicReference error = new AtomicReference<>(); final List frameTimes = Collections.synchronizedList(new ArrayList<>()); + final AtomicLong firstChunkNs = new AtomicLong(); private final VideoTrackSink videoSink = frame -> { frameTimes.add(System.nanoTime()); @@ -332,6 +344,7 @@ private final class Playback implements AutoCloseable { }; private final AudioTrackSink audioSink = (data, bits, rate, ch, count) -> { + firstChunkNs.compareAndSet(0, System.nanoTime()); chunks.incrementAndGet(); sampleRate.set(rate); channels.set(ch); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java index f39e3ead..b31f9f30 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/CustomVideoSourceAdaptationTests.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.Collections; +import java.util.Map; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -75,9 +76,14 @@ void lowBitrateScalesDown() throws Exception { Random random = new Random(1); byte[] noise = new byte[WIDTH * HEIGHT]; long frameUs = 1_000_000 / 30; - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); + // Generous, for runners slow enough to take a while over encoding + // 720p of noise; a source that adapts gets there within a second or + // two everywhere else. + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30); long nextStatsNs = 0; long sentWidth = WIDTH; + int pushed = 0; + Map outbound = Collections.emptyMap(); try { while (System.nanoTime() < deadline && sentWidth >= WIDTH) { @@ -92,17 +98,37 @@ void lowBitrateScalesDown() throws Exception { VideoFrame frame = new VideoFrame(buffer, 0); source.pushFrame(frame); frame.release(); + pushed++; if (System.nanoTime() > nextStatsNs) { nextStatsNs = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(500); - sentWidth = sentFrameWidth(caller.getPeerConnection()); + outbound = outboundVideoStats(caller.getPeerConnection()); + + Object width = outbound.get("frameWidth"); + + if (width instanceof Number) { + sentWidth = ((Number) width).longValue(); + } } Thread.sleep(frameUs / 1000); } - assertTrue(sentWidth < WIDTH, - "the encoder kept receiving " + WIDTH + "x" + HEIGHT + " frames"); + // What the sender reported last says whether it encoded at all and + // what held it back, which is what a failure on a platform that + // cannot be run locally has to go on. + assertTrue(sentWidth < WIDTH, String.format( + "the encoder kept receiving %dx%d frames; %d frames pushed, " + + "last outbound stats: framesEncoded=%s, framesSent=%s, " + + "framesPerSecond=%s, frameWidth=%s, targetBitrate=%s, " + + "qualityLimitationReason=%s, " + + "qualityLimitationResolutionChanges=%s, encoderImplementation=%s", + WIDTH, HEIGHT, pushed, outbound.get("framesEncoded"), + outbound.get("framesSent"), outbound.get("framesPerSecond"), + outbound.get("frameWidth"), outbound.get("targetBitrate"), + outbound.get("qualityLimitationReason"), + outbound.get("qualityLimitationResolutionChanges"), + outbound.get("encoderImplementation"))); } finally { sender.dispose(); @@ -114,24 +140,21 @@ void lowBitrateScalesDown() throws Exception { } /** - * Returns the width of the frames the sender encodes, or the full width - * while it has not reported one yet. + * Returns the sender's outbound video stats, or nothing while it has not + * reported any yet. */ - private static long sentFrameWidth(RTCPeerConnection peerConnection) throws Exception { + private static Map outboundVideoStats(RTCPeerConnection peerConnection) + throws Exception { CompletableFuture future = new CompletableFuture<>(); peerConnection.getStats(future::complete); for (RTCStats stats : future.get(2, TimeUnit.SECONDS).getStats().values()) { if (stats.getType() == RTCStatsType.OUTBOUND_RTP && "video".equals(stats.getAttributes().get("kind"))) { - Object width = stats.getAttributes().get("frameWidth"); - - if (width instanceof Number) { - return ((Number) width).longValue(); - } + return stats.getAttributes(); } } - return WIDTH; + return Collections.emptyMap(); } } From efe9f1d0353243ed301ca6c92e76fe48e885779e Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 12:33:35 +0200 Subject: [PATCH 19/33] fix: pass the stats timestamp to Java as the jlong its constructor takes RTCStats::toJava handed NewObject the webrtc::Timestamp itself where the Java constructor declares a long. NewObject takes C varargs, so nothing converted it. On 64-bit targets the object travels in the same register as a jlong would and the value came through intact. On 32-bit ARM a jlong is aligned to a register pair and the object is not, so every argument after it was shifted: each stats entry reached Java with a garbage type, a corrupted id string and garbage attributes. The report still had entries, which was all the existing test checked, but no entry could be found by its type, so the armv7 runner never saw any outbound RTP stats. The getStats test now checks that every entry carries a type, the id the report files it under, a timestamp and its attributes. --- webrtc-jni/src/main/cpp/src/api/RTCStats.cpp | 9 ++++++++- .../dev/onvoid/webrtc/RTCPeerConnectionTests.java | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/webrtc-jni/src/main/cpp/src/api/RTCStats.cpp b/webrtc-jni/src/main/cpp/src/api/RTCStats.cpp index 60bef5af..c3012124 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCStats.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCStats.cpp @@ -99,8 +99,15 @@ namespace jni RTC_LOG(LS_WARNING) << "No Java Enum for '" << stats.type() << "' found"; } + // NewObject takes its arguments as C varargs, which carry no type: + // the timestamp has to be the jlong the constructor declares, not + // the webrtc::Timestamp it is kept in. Passing the object happened + // to work where it fits in the same register as a jlong, but on + // 32-bit ARM a jlong is aligned to a register pair and the object + // is not, which shifted every argument after it and handed the + // constructor garbage for the type, the id and the attributes. jobject obj = env->NewObject(javaClass->cls, javaClass->ctor, - stats.timestamp(), + static_cast(stats.timestamp().us()), type ? type.get() : type, JavaString::toJava(env, stats.id()).get(), ((JavaLocalRef)attributeMap).get()); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java index 3885a7ec..0987f52f 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java @@ -430,6 +430,17 @@ void getStats() throws InterruptedException { assertNotNull(statsReport); assertNotNull(statsReport.getStats()); assertFalse(statsReport.getStats().isEmpty()); + + // Each entry is built from several native arguments at once, so a + // mismatch in how they are passed shows up as a missing type, an id + // that is not the one the report files it under, or no timestamp, + // rather than as an empty report. + statsReport.getStats().forEach((id, stats) -> { + assertNotNull(stats.getType(), "no type for " + id); + assertEquals(id, stats.getId()); + assertTrue(stats.getTimestamp() > 0, "no timestamp for " + id); + assertNotNull(stats.getAttributes(), "no attributes for " + id); + }); } @Test From 9fe9797be53f5bfa43ce6905cb2dff6a2a5662a6 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 13:59:52 +0200 Subject: [PATCH 20/33] build: ship the licenses of the bundled native code in the platform jars The platform jars carried FFmpeg's and WebRTC's binaries but none of the license texts those licenses ask to travel with them. The media platform jars now carry the LGPL text and a notice under META-INF/licenses/ffmpeg. The notice names the FFmpeg release the libraries are built from, unmodified, where its source is, and where the build configuration that selects its components lives, and points out that some of the formats it decodes may be covered by patents. The webrtc platform jars now carry, under META-INF/licenses/webrtc, the licenses of WebRTC and of the third-party code linked into it, generated from the build with WebRTC's own tool, and WebRTC's patent grant, which the tool leaves out. How to regenerate the file when the WebRTC branch changes is noted where it is installed. NOTICE, the media module's README and the media guide say where to find them. --- NOTICE | 13 + docs/guide/media/media-files.md | 4 + webrtc-java-media/README.md | 6 + webrtc-java-media/src/main/cpp/CMakeLists.txt | 27 + .../dependencies/ffmpeg/FFmpeg-NOTICE.txt.in | 39 + webrtc-jni/src/main/cpp/CMakeLists.txt | 24 + .../dependencies/webrtc/licenses/LICENSE.md | 2598 +++++++++++++++++ .../cpp/dependencies/webrtc/licenses/PATENTS | 24 + 8 files changed, 2735 insertions(+) create mode 100644 webrtc-java-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in create mode 100644 webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/LICENSE.md create mode 100644 webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/PATENTS diff --git a/NOTICE b/NOTICE index d8f064b2..b726bb05 100644 --- a/NOTICE +++ b/NOTICE @@ -6,6 +6,19 @@ EXTERNAL COMPONENTS This product bundles WebRTC, which is available under a "3-clause BSD" license. For details, see https://webrtc.github.io/webrtc-org/license. +WebRTC is linked statically into the webrtc-java platform jars, together with +the third-party code it builds in, among them libvpx, libyuv, OpenH264, libaom, +dav1d, Opus, BoringSSL, Abseil and, for H.264 decoding, Chromium's FFmpeg under +the GNU Lesser General Public License version 2.1 or later. Each platform jar +carries their licenses and WebRTC's patent grant under META-INF/licenses/webrtc. + +The webrtc-java-media platform jars bundle FFmpeg (https://ffmpeg.org), which is +available under the GNU Lesser General Public License version 2.1 or later. It is +built from an unmodified FFmpeg release, without --enable-gpl and without +--enable-nonfree, and linked dynamically, so it can be replaced. Each platform +jar carries the license text and a notice naming the FFmpeg version and where +its source is, under META-INF/licenses/ffmpeg. + DOCUMENTATION WebRTC 1.0: Real-time Communication Between Browsers diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index 2c19a3ce..ac05c425 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -229,6 +229,10 @@ Only local files play today. FFmpeg demuxes network sources just as well, so the The module uses FFmpeg under the LGPL version 2.1 or later. It is configured without `--enable-gpl` and without `--enable-nonfree`, and FFmpeg is linked dynamically and shipped as separate files inside the platform jar, so its libraries may be replaced with your own build, as the LGPL requires. The wrapper code is licensed under the Apache License 2.0 like the rest of webrtc-java. +Each platform jar carries the LGPL text and a notice under `META-INF/licenses/ffmpeg`, naming the FFmpeg release the libraries are built from, unmodified, and where its source is. If you redistribute your application with these jars, keep those files with them. + +Some of the formats FFmpeg decodes, such as H.264, H.265/HEVC and AAC, may be covered by patents in some countries. Whether your use of them needs a patent license is for you to determine. + ## Complete Example 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. diff --git a/webrtc-java-media/README.md b/webrtc-java-media/README.md index 47bbaff5..c496a8f8 100644 --- a/webrtc-java-media/README.md +++ b/webrtc-java-media/README.md @@ -37,6 +37,12 @@ FFmpeg is linked dynamically and its libraries ship as separate files inside the you may replace them with your own build, as the LGPL requires. The wrapper code in this module is licensed under the Apache License 2.0 like the rest of webrtc-java. +Each platform jar carries the LGPL text and a notice under `META-INF/licenses/ffmpeg`. The notice +names the FFmpeg release the libraries are built from, unmodified, and where its source is; how it +is configured is in `src/main/cpp/dependencies/ffmpeg/CMakeLists.txt`. Some of the formats FFmpeg +decodes, such as H.264, H.265/HEVC and AAC, may be covered by patents in some countries, and whether +your use of them needs a patent license is for you to determine. + ## Building This module is part of the normal build, and it builds FFmpeg from a submodule, so the submodule diff --git a/webrtc-java-media/src/main/cpp/CMakeLists.txt b/webrtc-java-media/src/main/cpp/CMakeLists.txt index f338320c..e603fa37 100644 --- a/webrtc-java-media/src/main/cpp/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/CMakeLists.txt @@ -138,3 +138,30 @@ foreach(RUNTIME_LIB ${FFMPEG_RUNTIME_LIBS}) "${RUNTIME_LIB}" "${NATIVES_DIR}" COMMENT "Collecting ${RUNTIME_LIB} for the platform jar") endforeach() + +# +# The LGPL asks for its text to travel with the FFmpeg libraries, along with +# a notice saying what they are and where their source is. Both go under +# META-INF, where they cannot collide with anything on a classpath. +# +set(FFMPEG_LICENSE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../../third-party/ffmpeg/COPYING.LGPLv2.1") + +if(NOT EXISTS "${FFMPEG_LICENSE_FILE}") + message(FATAL_ERROR + "${FFMPEG_LICENSE_FILE} is missing, and the platform jar must not ship FFmpeg " + "without it. Check out the FFmpeg submodule.") +endif() + +set(FFMPEG_NOTICE_FILE "${CMAKE_CURRENT_BINARY_DIR}/FFmpeg-NOTICE.txt") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in" + "${FFMPEG_NOTICE_FILE}" @ONLY) + +set(FFMPEG_LICENSES_DIR "${NATIVES_DIR}/META-INF/licenses/ffmpeg") + +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${FFMPEG_LICENSES_DIR}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FFMPEG_LICENSE_FILE}" "${FFMPEG_LICENSES_DIR}/COPYING.LGPLv2.1" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FFMPEG_NOTICE_FILE}" "${FFMPEG_LICENSES_DIR}/NOTICE.txt" + COMMENT "Collecting the FFmpeg license and notice for the platform jar") diff --git a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in new file mode 100644 index 00000000..b675cd17 --- /dev/null +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/FFmpeg-NOTICE.txt.in @@ -0,0 +1,39 @@ +FFmpeg +====== + +The libraries avcodec, avformat, avutil, swresample and swscale in this jar +are FFmpeg @FFMPEG_VERSION@ (https://ffmpeg.org), licensed under the GNU Lesser +General Public License version 2.1 or later. The full text of that license is in +COPYING.LGPLv2.1, next to this file. + +They are built without --enable-gpl and without --enable-nonfree, so no part +of them is under the GPL or a non-free license. + +They are linked dynamically: the webrtc-java-media library loads them as +separate shared libraries, which you may replace with your own build of a +compatible FFmpeg version. + + +Source code +----------- + +The libraries are built from the FFmpeg release n@FFMPEG_VERSION@, without +modifications: + + https://github.com/FFmpeg/FFmpeg/tree/n@FFMPEG_VERSION@ + https://ffmpeg.org/releases/ffmpeg-@FFMPEG_VERSION@.tar.xz + +How they are configured and built, including which demuxers, decoders, +parsers and protocols are enabled, is set out in +webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt of +webrtc-java, at the release this jar belongs to: + + https://github.com/devopvoid/webrtc-java + + +Patents +------- + +Some of the formats FFmpeg decodes, such as H.264, H.265/HEVC and AAC, may be +covered by patents in some countries. Whether your use of them needs a patent +license is for you to determine. diff --git a/webrtc-jni/src/main/cpp/CMakeLists.txt b/webrtc-jni/src/main/cpp/CMakeLists.txt index e129958a..45f27c81 100644 --- a/webrtc-jni/src/main/cpp/CMakeLists.txt +++ b/webrtc-jni/src/main/cpp/CMakeLists.txt @@ -123,3 +123,27 @@ install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} COMPONENT Runtime LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX} COMPONENT Runtime ) + +# +# The native library links WebRTC statically, and with it the third-party +# code WebRTC builds in. Their licenses ask for their text to travel with the +# binary, so it goes into the platform jar, under META-INF where it cannot +# collide with anything on a classpath. +# +# LICENSE.md is generated from the WebRTC build rather than written by hand, +# so that it lists exactly what is linked in. Regenerate it whenever +# webrtc.branch changes, from the WebRTC checkout (depot_tools on the PATH): +# +# python tools_webrtc/libs/generate_licenses.py --target //:webrtc out/ +# +# The tool knows no license for ffmpeg and openh264, which rtc_use_h264 pulls +# in, and stops; map them to third_party/ffmpeg/COPYING.LGPLv2.1 and +# third_party/openh264/src/LICENSE in its LIB_TO_LICENSES_DICT first. +# PATENTS is WebRTC's additional patent grant, which the tool leaves out. +# +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/dependencies/webrtc/licenses/LICENSE.md" + "${CMAKE_CURRENT_SOURCE_DIR}/dependencies/webrtc/licenses/PATENTS" + DESTINATION "${CMAKE_INSTALL_PREFIX}/META-INF/licenses/webrtc" + COMPONENT Runtime +) diff --git a/webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/LICENSE.md b/webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/LICENSE.md new file mode 100644 index 00000000..e017e34f --- /dev/null +++ b/webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/LICENSE.md @@ -0,0 +1,2598 @@ +# webrtc +``` +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +# abseil-cpp +``` + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + https://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. + + +``` + +# boringssl +``` + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +Licenses for support code +------------------------- + +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +# compiler-rt +``` +============================================================================== +The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the LLVM Project: +============================================================================== +The LLVM Project contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. + +============================================================================== +Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy): +============================================================================== + +The compiler_rt library is dual licensed under both the University of Illinois +"BSD-Like" license and the MIT license. As a user of this code you may choose +to use it under either license. As a contributor, you agree to allow your code +to be used under both. + +Full text of the relevant licenses is included below. + +============================================================================== + +University of Illinois/NCSA +Open Source License + +Copyright (c) 2009-2019 by the contributors listed in CREDITS.TXT + +All rights reserved. + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +============================================================================== + +Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + +# dav1d +``` +Copyright © 2018, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +# ffmpeg +``` + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + +``` + +# fft +``` +/* + * Copyright(c)1995,97 Mark Olesen <olesen@me.QueensU.CA> + * Queen's Univ at Kingston (Canada) + * + * Permission to use, copy, modify, and distribute this software for + * any purpose without fee is hereby granted, provided that this + * entire notice is included in all copies of any software which is + * or includes a copy or modification of this software and in all + * copies of the supporting documentation for such software. + * + * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR + * IMPLIED WARRANTY. IN PARTICULAR, NEITHER THE AUTHOR NOR QUEEN'S + * UNIVERSITY AT KINGSTON MAKES ANY REPRESENTATION OR WARRANTY OF ANY + * KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS + * FITNESS FOR ANY PARTICULAR PURPOSE. + * + * All of which is to say that you can do what you like with this + * source code provided you don't try to sell it as your own and you + * include an unaltered copy of this message (including the + * copyright). + * + * It is also implicitly understood that bug fixes and improvements + * should make their way back to the general Internet community so + * that everyone benefits. + */ + +``` + +# fiat +``` +The Apache License, Version 2.0 (Apache-2.0) + +Copyright 2015-2020 the fiat-crypto authors (see the AUTHORS file) + +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. + +``` + +# g711 +``` +/* + * SpanDSP - a series of DSP components for telephony + * + * g711.h - In line A-law and u-law conversion routines + * + * Written by Steve Underwood <steveu@coppice.org> + * + * Copyright (C) 2001 Steve Underwood + * + * Despite my general liking of the GPL, I place this code in the + * public domain for the benefit of all mankind - even the slimy + * ones who might try to proprietize my work and use it to my + * detriment. + */ + +``` + +# g722 +``` +/* + * SpanDSP - a series of DSP components for telephony + * + * g722_decode.c - The ITU G.722 codec, decode part. + * + * Written by Steve Underwood <steveu@coppice.org> + * + * Copyright (C) 2005 Steve Underwood + * + * Despite my general liking of the GPL, I place my own contributions + * to this code in the public domain for the benefit of all mankind - + * even the slimy ones who might try to proprietize my work and use it + * to my detriment. + * + * Based in part on a single channel G.722 codec which is: + * + * Copyright (c) CMU 1993 + * Computer Science, Speech Group + * Chengxiang Lu and Alex Hauptmann + */ + +``` + +# libaom +``` +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +``` + +# libc++ +``` +============================================================================== +The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the LLVM Project: +============================================================================== +The LLVM Project contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. + +============================================================================== +Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy): +============================================================================== + +The libc++ library is dual licensed under both the University of Illinois +"BSD-Like" license and the MIT license. As a user of this code you may choose +to use it under either license. As a contributor, you agree to allow your code +to be used under both. + +Full text of the relevant licenses is included below. + +============================================================================== + +University of Illinois/NCSA +Open Source License + +Copyright (c) 2009-2019 by the contributors listed in CREDITS.TXT + +All rights reserved. + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +============================================================================== + +Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +``` + +# libjpeg_turbo +``` +libjpeg-turbo Licenses +====================== + +libjpeg-turbo is covered by two compatible BSD-style open source licenses: + +- The IJG (Independent JPEG Group) License, which is listed in + [README.ijg](README.ijg) + + This license applies to the libjpeg API library and associated programs, + including any code inherited from libjpeg and any modifications to that + code. Note that the libjpeg-turbo SIMD source code bears the + [zlib License](https://opensource.org/licenses/Zlib), but in the context of + the overall libjpeg API library, the terms of the zlib License are subsumed + by the terms of the IJG License. + +- The Modified (3-clause) BSD License, which is listed below + + This license applies to the TurboJPEG API library and associated programs, as + well as the build system. Note that the TurboJPEG API library wraps the + libjpeg API library, so in the context of the overall TurboJPEG API library, + both the terms of the IJG License and the terms of the Modified (3-clause) + BSD License apply. + + +Complying with the libjpeg-turbo Licenses +========================================= + +This section provides a roll-up of the libjpeg-turbo licensing terms, to the +best of our understanding. This is not a license in and of itself. It is +intended solely for clarification. + +1. If you are distributing a modified version of the libjpeg-turbo source, + then: + + 1. You cannot alter or remove any existing copyright or license notices + from the source. + + **Origin** + - Clause 1 of the IJG License + - Clause 1 of the Modified BSD License + - Clauses 1 and 3 of the zlib License + + 2. You must add your own copyright notice to the header of each source + file you modified, so others can tell that you modified that file. (If + there is not an existing copyright header in that file, then you can + simply add a notice stating that you modified the file.) + + **Origin** + - Clause 1 of the IJG License + - Clause 2 of the zlib License + + 3. You must include the IJG README file, and you must not alter any of the + copyright or license text in that file. + + **Origin** + - Clause 1 of the IJG License + +2. If you are distributing only libjpeg-turbo binaries without the source, or + if you are distributing an application that statically links with + libjpeg-turbo, then: + + 1. Your product documentation must include a message stating: + + This software is based in part on the work of the Independent JPEG + Group. + + **Origin** + - Clause 2 of the IJG license + + 2. If your binary distribution includes or uses the TurboJPEG API, then + your product documentation must include the text of the Modified BSD + License (see below.) + + **Origin** + - Clause 2 of the Modified BSD License + +3. You cannot use the name of the IJG or The libjpeg-turbo Project or the + contributors thereof in advertising, publicity, etc. + + **Origin** + - IJG License + - Clause 3 of the Modified BSD License + +4. The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be + free of defects, nor do we accept any liability for undesirable + consequences resulting from your use of the software. + + **Origin** + - IJG License + - Modified BSD License + - zlib License + + +The Modified (3-clause) BSD License +=================================== + +Copyright (C)2009-2024 D. R. Commander. All Rights Reserved.<br> +Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +Why Two Licenses? +================= + +The zlib License could have been used instead of the Modified (3-clause) BSD +License, and since the IJG License effectively subsumes the distribution +conditions of the zlib License, this would have effectively placed +libjpeg-turbo binary distributions under the IJG License. However, the IJG +License specifically refers to the Independent JPEG Group and does not extend +attribution and endorsement protections to other entities. Thus, it was +desirable to choose a license that granted us the same protections for new code +that were granted to the IJG for code derived from their software. + +``` + +# libsrtp +``` +/* + * + * Copyright (c) 2001-2017 Cisco Systems, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * Neither the name of the Cisco Systems, Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +``` + +# libvpx +``` +Copyright (c) 2010, The WebM Project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google, nor the WebM Project, nor the names + of its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +``` + +# libyuv +``` +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +# nasm +``` +NASM is now licensed under the 2-clause BSD license, also known as the +simplified BSD license. + +SPDX-License-Identifier: BSD-2-Clause + + Copyright 1996-2025 the NASM Authors - All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following + conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +# ooura +``` +/* + * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html + * Copyright Takuya OOURA, 1996-2001 + * + * You may use, copy, modify and distribute this code for any purpose (include + * commercial use) and without fee. Please refer to this package when you modify + * this code. + */ + +``` + +# openh264 +``` +Copyright (c) 2013, Cisco Systems +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +# opus +``` +Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic, + Jean-Marc Valin, Timothy B. Terriberry, + CSIRO, Gregory Maxwell, Mark Borgerding, + Erik de Castro Lopo, Mozilla, Amazon + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of Internet Society, IETF or IETF Trust, nor the +names of specific contributors, may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Opus is subject to the royalty-free patent licenses which are +specified at: + +Xiph.Org Foundation: +https://datatracker.ietf.org/ipr/1524/ + +Microsoft Corporation: +https://datatracker.ietf.org/ipr/1914/ + +Broadcom Corporation: +https://datatracker.ietf.org/ipr/1526/ + +``` + +# perfetto +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +------------------ + +Files: * except those files noted below + + Copyright (c) 2017, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + 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. + + +------------------ + +Files: src/trace_processor/perfetto_sql/stdlib/chromium/*, protos/third_party/chromium/*, test/trace_processor/diff_tests/stdlib/chrome/* + + Copyright 2015 The Chromium Authors + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------ + +Files: src/trace_processor/perfetto_sql/syntaqlite/syntaqlite_perfetto.{c, h} + + Copyright 2025 The syntaqlite Authors. All rights reserved. + + Machine-generated amalgamation of syntaqlite runtime + Perfetto dialect + sources (https://github.com/LalitMaganti/syntaqlite). Portions derive + from SQLite's public-domain `parse.y` grammar. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + 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. + + +------------------ + +Files: src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.{c, h} + +The author disclaims copyright to this source code. In place of a legal notice, here is a blessing: + +May you do good and not evil. +May you find forgiveness for yourself and forgive others. +May you share freely, never taking more than you give. + + +------------------ + +Files: src/base/intrusive_tree.cc + +$OpenBSD: tree.h,v 1.31 2023/03/08 04:43:09 guenther Exp $ + +Copyright 2002 Niels Provos <provos@citi.umich.edu> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +# pffft +``` +Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + +Based on original fortran 77 code from FFTPACKv4 from NETLIB, +authored by Dr Paul Swarztrauber of NCAR, in 1985. + +As confirmed by the NCAR fftpack software curators, the following +FFTPACKv5 license applies to FFTPACKv4 sources. My changes are +released under the same terms. + +FFTPACK license: + +http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + +Copyright (c) 2004 the University Corporation for Atmospheric +Research ("UCAR"). All rights reserved. Developed by NCAR's +Computational and Information Systems Laboratory, UCAR, +www.cisl.ucar.edu. + +Redistribution and use of the Software in source and binary forms, +with or without modification, is permitted provided that the +following conditions are met: + +- Neither the names of NCAR's Computational and Information Systems +Laboratory, the University Corporation for Atmospheric Research, +nor the names of its sponsors or contributors may be used to +endorse or promote products derived from this Software without +specific prior written permission. + +- Redistributions of source code must retain the above copyright +notices, this list of conditions, and the disclaimer below. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions, and the disclaimer below in the +documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +``` + +# protobuf +``` +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +``` + +# rnnoise +``` +Copyright (c) 2017, Mozilla +Copyright (c) 2007-2017, Jean-Marc Valin +Copyright (c) 2005-2017, Xiph.Org Foundation +Copyright (c) 2003-2004, Mark Borgerding + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.Org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +# spl_sqrt_floor +``` +/* + * Written by Wilco Dijkstra, 1996. The following email exchange establishes the + * license. + * + * From: Wilco Dijkstra <Wilco.Dijkstra@ntlworld.com> + * Date: Fri, Jun 24, 2011 at 3:20 AM + * Subject: Re: sqrt routine + * To: Kevin Ma <kma@google.com> + * Hi Kevin, + * Thanks for asking. Those routines are public domain (originally posted to + * comp.sys.arm a long time ago), so you can use them freely for any purpose. + * Cheers, + * Wilco + * + * ----- Original Message ----- + * From: "Kevin Ma" <kma@google.com> + * To: <Wilco.Dijkstra@ntlworld.com> + * Sent: Thursday, June 23, 2011 11:44 PM + * Subject: Fwd: sqrt routine + * Hi Wilco, + * I saw your sqrt routine from several web sites, including + * http://www.finesse.demon.co.uk/steven/sqrt.html. + * Just wonder if there's any copyright information with your Successive + * approximation routines, or if I can freely use it for any purpose. + * Thanks. + * Kevin + */ + +``` + diff --git a/webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/PATENTS b/webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/PATENTS new file mode 100644 index 00000000..190607ac --- /dev/null +++ b/webrtc-jni/src/main/cpp/dependencies/webrtc/licenses/PATENTS @@ -0,0 +1,24 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the WebRTC code package. + +Google hereby grants to you a perpetual, worldwide, non-exclusive, +no-charge, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, +transfer, and otherwise run, modify and propagate the contents of this +implementation of the WebRTC code package, where such license applies +only to those patent claims, both currently owned by Google and +acquired in the future, licensable by Google that are necessarily +infringed by this implementation of the WebRTC code package. This +grant does not include claims that would be infringed only as a +consequence of further modification of this implementation. If you or +your agent or exclusive licensee institute or order or agree to the +institution of patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that this +implementation of the WebRTC code package or any code incorporated +within this implementation of the WebRTC code package constitutes +direct or contributory patent infringement, or inducement of patent +infringement, then any patent rights granted to you under this License +for this implementation of the WebRTC code package shall terminate as +of the date such litigation is filed. From 8aa3b67b3afff52f43eb47ad609c6acc15c2a27d Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 14:55:41 +0200 Subject: [PATCH 21/33] refactor: move the media module's classes to dev.onvoid.webrtc.media.player The package was named after the library behind it rather than what it offers. The classes read media files and network streams and play them into a peer connection, so they now live in dev.onvoid.webrtc.media.player. dev.onvoid.webrtc.media itself cannot be used: the webrtc.java module already owns that package, and a second module carrying it would be a split package that the module system rejects. The native entry points, the MediaInfo lookup, the examples and the media guide follow the new name. --- docs/guide/media/media-files.md | 14 +++---- .../webrtc/examples/MediaFileExample.java | 8 ++-- .../examples/MediaFilePlayerExample.java | 8 ++-- .../src/main/cpp/include/JNI_FFmpeg.h | 14 +++---- .../src/main/cpp/include/JNI_MediaPlayer.h | 38 +++++++++---------- .../src/main/cpp/include/JNI_MediaReader.h | 20 +++++----- .../src/main/cpp/src/JNI_FFmpeg.cpp | 4 +- .../src/main/cpp/src/JNI_MediaPlayer.cpp | 16 ++++---- .../src/main/cpp/src/JNI_MediaReader.cpp | 8 ++-- .../media/{ffmpeg => player}/FFmpeg.java | 2 +- .../{ffmpeg => player}/MediaFileSource.java | 2 +- .../media/{ffmpeg => player}/MediaInfo.java | 2 +- .../media/{ffmpeg => player}/MediaPlayer.java | 2 +- .../MediaPlayerListener.java | 2 +- .../{ffmpeg => player}/MediaPlayerState.java | 2 +- .../media/{ffmpeg => player}/MediaReader.java | 2 +- .../src/main/java/module-info.java | 2 +- .../MediaFileSourceTest.java | 2 +- .../{ffmpeg => player}/MediaPlayerTest.java | 2 +- .../{ffmpeg => player}/MediaReaderTest.java | 2 +- 20 files changed, 76 insertions(+), 76 deletions(-) rename webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/{ffmpeg => player}/FFmpeg.java (98%) rename webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaFileSource.java (99%) rename webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaInfo.java (99%) rename webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaPlayer.java (99%) rename webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaPlayerListener.java (97%) rename webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaPlayerState.java (97%) rename webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaReader.java (98%) rename webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaFileSourceTest.java (99%) rename webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaPlayerTest.java (99%) rename webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/{ffmpeg => player}/MediaReaderTest.java (99%) diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index ac05c425..895a6c50 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -58,7 +58,7 @@ Unlike `webrtc-java`, which brings its natives along by itself, this module cann ```java // Import required classes -import dev.onvoid.webrtc.media.ffmpeg.MediaFileSource; +import dev.onvoid.webrtc.media.player.MediaFileSource; import dev.onvoid.webrtc.media.audio.AudioTrack; import dev.onvoid.webrtc.media.video.VideoTrack; import java.nio.file.Path; @@ -101,8 +101,8 @@ PeerConnectionFactory factory = new PeerConnectionFactory(audioModule); ```java // Import required classes -import dev.onvoid.webrtc.media.ffmpeg.MediaInfo; -import dev.onvoid.webrtc.media.ffmpeg.MediaReader; +import dev.onvoid.webrtc.media.player.MediaInfo; +import dev.onvoid.webrtc.media.player.MediaReader; try (MediaReader reader = new MediaReader(Path.of("movie.mp4"))) { MediaInfo info = reader.getInfo(); @@ -149,8 +149,8 @@ A seek lands on the keyframe at or before the position asked for, which is how f ```java // Import required classes -import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerListener; -import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerState; +import dev.onvoid.webrtc.media.player.MediaPlayerListener; +import dev.onvoid.webrtc.media.player.MediaPlayerState; source.setListener(new MediaPlayerListener() { @@ -181,8 +181,8 @@ Every call arrives on the player's own thread, and that thread is the one decodi ```java // Import required classes -import dev.onvoid.webrtc.media.ffmpeg.MediaPlayer; -import dev.onvoid.webrtc.media.ffmpeg.MediaReader; +import dev.onvoid.webrtc.media.player.MediaPlayer; +import dev.onvoid.webrtc.media.player.MediaReader; import dev.onvoid.webrtc.media.video.CustomVideoSource; CustomVideoSource videoSource = new CustomVideoSource(); diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java index cdbc3b05..7f8e62e0 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java @@ -32,10 +32,10 @@ import dev.onvoid.webrtc.media.audio.AudioDeviceModule; import dev.onvoid.webrtc.media.audio.AudioLayer; import dev.onvoid.webrtc.media.audio.AudioTrack; -import dev.onvoid.webrtc.media.ffmpeg.MediaFileSource; -import dev.onvoid.webrtc.media.ffmpeg.MediaInfo; -import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerListener; -import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerState; +import dev.onvoid.webrtc.media.player.MediaFileSource; +import dev.onvoid.webrtc.media.player.MediaInfo; +import dev.onvoid.webrtc.media.player.MediaPlayerListener; +import dev.onvoid.webrtc.media.player.MediaPlayerState; import dev.onvoid.webrtc.media.video.VideoTrack; import java.util.List; diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java index 5dd8160d..a0c30df8 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java @@ -82,10 +82,10 @@ import dev.onvoid.webrtc.media.audio.AudioDeviceModule; import dev.onvoid.webrtc.media.audio.AudioLayer; import dev.onvoid.webrtc.media.audio.AudioTrack; -import dev.onvoid.webrtc.media.ffmpeg.MediaFileSource; -import dev.onvoid.webrtc.media.ffmpeg.MediaInfo; -import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerListener; -import dev.onvoid.webrtc.media.ffmpeg.MediaPlayerState; +import dev.onvoid.webrtc.media.player.MediaFileSource; +import dev.onvoid.webrtc.media.player.MediaInfo; +import dev.onvoid.webrtc.media.player.MediaPlayerListener; +import dev.onvoid.webrtc.media.player.MediaPlayerState; import dev.onvoid.webrtc.media.video.VideoBufferConverter; import dev.onvoid.webrtc.media.video.VideoFrame; import dev.onvoid.webrtc.media.video.VideoFrameBuffer; diff --git a/webrtc-java-media/src/main/cpp/include/JNI_FFmpeg.h b/webrtc-java-media/src/main/cpp/include/JNI_FFmpeg.h index 84234f0a..0ceb0092 100644 --- a/webrtc-java-media/src/main/cpp/include/JNI_FFmpeg.h +++ b/webrtc-java-media/src/main/cpp/include/JNI_FFmpeg.h @@ -15,27 +15,27 @@ */ #include -/* Header for class dev_onvoid_webrtc_media_ffmpeg_FFmpeg */ +/* Header for class dev_onvoid_webrtc_media_player_FFmpeg */ -#ifndef _Included_dev_onvoid_webrtc_media_ffmpeg_FFmpeg -#define _Included_dev_onvoid_webrtc_media_ffmpeg_FFmpeg +#ifndef _Included_dev_onvoid_webrtc_media_player_FFmpeg +#define _Included_dev_onvoid_webrtc_media_player_FFmpeg #ifdef __cplusplus extern "C" { #endif /* - * Class: dev_onvoid_webrtc_media_ffmpeg_FFmpeg + * Class: dev_onvoid_webrtc_media_player_FFmpeg * Method: version * Signature: ()Ljava/lang/String; */ - JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_FFmpeg_version + JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_player_FFmpeg_version (JNIEnv *, jclass); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_FFmpeg + * Class: dev_onvoid_webrtc_media_player_FFmpeg * Method: license * Signature: ()Ljava/lang/String; */ - JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_FFmpeg_license + JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_player_FFmpeg_license (JNIEnv *, jclass); #ifdef __cplusplus diff --git a/webrtc-java-media/src/main/cpp/include/JNI_MediaPlayer.h b/webrtc-java-media/src/main/cpp/include/JNI_MediaPlayer.h index fd080bd9..f1222c88 100644 --- a/webrtc-java-media/src/main/cpp/include/JNI_MediaPlayer.h +++ b/webrtc-java-media/src/main/cpp/include/JNI_MediaPlayer.h @@ -15,75 +15,75 @@ */ #include -/* Header for class dev_onvoid_webrtc_media_ffmpeg_MediaPlayer */ +/* Header for class dev_onvoid_webrtc_media_player_MediaPlayer */ -#ifndef _Included_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer -#define _Included_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer +#ifndef _Included_dev_onvoid_webrtc_media_player_MediaPlayer +#define _Included_dev_onvoid_webrtc_media_player_MediaPlayer #ifdef __cplusplus extern "C" { #endif /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: create * Signature: (JJJJ)J */ - JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_create + JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create (JNIEnv *, jobject, jlong, jlong, jlong, jlong); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: start * Signature: (J)V */ - JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_start + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_start (JNIEnv *, jclass, jlong); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: suspend * Signature: (J)V */ - JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_suspend + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_suspend (JNIEnv *, jclass, jlong); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: seek * Signature: (JJ)V */ - JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_seek + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_seek (JNIEnv *, jclass, jlong, jlong); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: setLooping * Signature: (JZ)V */ - JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_setLooping + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_setLooping (JNIEnv *, jclass, jlong, jboolean); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: position * Signature: (J)J */ - JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_position + JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_position (JNIEnv *, jclass, jlong); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: state * Signature: (J)I */ - JNIEXPORT jint JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_state + JNIEXPORT jint JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_state (JNIEnv *, jclass, jlong); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaPlayer + * Class: dev_onvoid_webrtc_media_player_MediaPlayer * Method: dispose * Signature: (J)V */ - JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_dispose + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_dispose (JNIEnv *, jclass, jlong); #ifdef __cplusplus diff --git a/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h b/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h index 77d8badc..360ea570 100644 --- a/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h +++ b/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h @@ -15,35 +15,35 @@ */ #include -/* Header for class dev_onvoid_webrtc_media_ffmpeg_MediaReader */ +/* Header for class dev_onvoid_webrtc_media_player_MediaReader */ -#ifndef _Included_dev_onvoid_webrtc_media_ffmpeg_MediaReader -#define _Included_dev_onvoid_webrtc_media_ffmpeg_MediaReader +#ifndef _Included_dev_onvoid_webrtc_media_player_MediaReader +#define _Included_dev_onvoid_webrtc_media_player_MediaReader #ifdef __cplusplus extern "C" { #endif /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaReader + * Class: dev_onvoid_webrtc_media_player_MediaReader * Method: open * Signature: (Ljava/lang/String;)J */ - JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_open + JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_open (JNIEnv *, jclass, jstring); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaReader + * Class: dev_onvoid_webrtc_media_player_MediaReader * Method: info - * Signature: (J)Ldev/onvoid/webrtc/media/ffmpeg/MediaInfo; + * Signature: (J)Ldev/onvoid/webrtc/media/player/MediaInfo; */ - JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_info + JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_info (JNIEnv *, jclass, jlong); /* - * Class: dev_onvoid_webrtc_media_ffmpeg_MediaReader + * Class: dev_onvoid_webrtc_media_player_MediaReader * Method: dispose * Signature: (J)V */ - JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_dispose + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_dispose (JNIEnv *, jclass, jlong); #ifdef __cplusplus diff --git a/webrtc-java-media/src/main/cpp/src/JNI_FFmpeg.cpp b/webrtc-java-media/src/main/cpp/src/JNI_FFmpeg.cpp index dd8c7285..e66a1d47 100644 --- a/webrtc-java-media/src/main/cpp/src/JNI_FFmpeg.cpp +++ b/webrtc-java-media/src/main/cpp/src/JNI_FFmpeg.cpp @@ -21,7 +21,7 @@ extern "C" { #include } -JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_FFmpeg_version +JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_player_FFmpeg_version (JNIEnv * env, jclass caller) { // The version of the libraries that were actually loaded, which is not @@ -30,7 +30,7 @@ JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_FFmpeg_version return env->NewStringUTF(av_version_info()); } -JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_FFmpeg_license +JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_player_FFmpeg_license (JNIEnv * env, jclass caller) { return env->NewStringUTF(avformat_license()); diff --git a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp index 13a6e2a6..47afe7f7 100644 --- a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp +++ b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp @@ -56,7 +56,7 @@ namespace } } -JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_create +JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create (JNIEnv * env, jobject caller, jlong readerHandle, jlong tableAddress, jlong videoSourceHandle, jlong audioSourceHandle) { @@ -104,7 +104,7 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_create return reinterpret_cast(player.release()); } -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_start +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_start (JNIEnv * env, jclass caller, jlong handle) { if (handle != 0) { @@ -112,7 +112,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_start } } -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_suspend +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_suspend (JNIEnv * env, jclass caller, jlong handle) { if (handle != 0) { @@ -120,7 +120,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_suspend } } -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_seek +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_seek (JNIEnv * env, jclass caller, jlong handle, jlong positionUs) { if (handle != 0) { @@ -128,7 +128,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_seek } } -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_setLooping +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_setLooping (JNIEnv * env, jclass caller, jlong handle, jboolean looping) { if (handle != 0) { @@ -136,13 +136,13 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_setLoopin } } -JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_position +JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_position (JNIEnv * env, jclass caller, jlong handle) { return handle != 0 ? PlayerOf(handle)->GetPositionUs() : 0; } -JNIEXPORT jint JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_state +JNIEXPORT jint JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_state (JNIEnv * env, jclass caller, jlong handle) { // A player that is gone is closed, which is what the Java side reports @@ -150,7 +150,7 @@ JNIEXPORT jint JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_state return handle != 0 ? PlayerOf(handle)->GetState() : ffmpeg::kClosed; } -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaPlayer_dispose +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_dispose (JNIEnv * env, jclass caller, jlong handle) { // Closing twice is allowed, so a handle that is already zero is simply diff --git a/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp b/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp index 3e952f7a..ee0bba0d 100644 --- a/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp +++ b/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp @@ -66,7 +66,7 @@ namespace } } -JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_open +JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_open (JNIEnv * env, jclass caller, jstring source) { if (source == nullptr) { @@ -100,7 +100,7 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_open return reinterpret_cast(reader); } -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_info +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_info (JNIEnv * env, jclass caller, jlong handle) { ffmpeg::MediaReader * reader = ReaderOf(env, handle); @@ -109,7 +109,7 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_info return nullptr; } - jclass cls = env->FindClass("dev/onvoid/webrtc/media/ffmpeg/MediaInfo"); + jclass cls = env->FindClass("dev/onvoid/webrtc/media/player/MediaInfo"); if (cls == nullptr) { return nullptr; @@ -153,7 +153,7 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_info return info; } -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_ffmpeg_MediaReader_dispose +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_dispose (JNIEnv * env, jclass caller, jlong handle) { // Closing twice is allowed, so a handle that is already zero is simply diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/FFmpeg.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/FFmpeg.java similarity index 98% rename from webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/FFmpeg.java rename to webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/FFmpeg.java index 1151addb..b738a4c5 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/FFmpeg.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/FFmpeg.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; import dev.onvoid.webrtc.internal.NativeLoader; diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaFileSource.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java similarity index 99% rename from webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaFileSource.java rename to webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java index 1fced442..3573f236 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaFileSource.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; import java.io.IOException; import java.nio.file.Path; diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaInfo.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaInfo.java similarity index 99% rename from webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaInfo.java rename to webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaInfo.java index eb5d5ca6..b9d42269 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaInfo.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaInfo.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; /** * What a media source contains: how long it runs, and the format of the video diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayer.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java similarity index 99% rename from webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayer.java rename to webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java index cd09f8cc..4681f64f 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayer.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; import java.io.IOException; import java.util.Objects; diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerListener.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java similarity index 97% rename from webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerListener.java rename to webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java index 7e7ab7fe..4e43611b 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerListener.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; /** * What a {@link MediaPlayer} reports while it runs. diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerState.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerState.java similarity index 97% rename from webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerState.java rename to webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerState.java index 682b7344..b197376d 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerState.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerState.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; /** * What a {@link MediaPlayer} is currently doing. diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaReader.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaReader.java similarity index 98% rename from webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaReader.java rename to webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaReader.java index da50e429..4021ff7c 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/ffmpeg/MediaReader.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaReader.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; import java.io.IOException; import java.nio.file.Path; diff --git a/webrtc-java-media/src/main/java/module-info.java b/webrtc-java-media/src/main/java/module-info.java index e48c075b..7ea0e974 100644 --- a/webrtc-java-media/src/main/java/module-info.java +++ b/webrtc-java-media/src/main/java/module-info.java @@ -6,6 +6,6 @@ requires webrtc.java; - exports dev.onvoid.webrtc.media.ffmpeg; + exports dev.onvoid.webrtc.media.player; } diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaFileSourceTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaFileSourceTest.java similarity index 99% rename from webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaFileSourceTest.java rename to webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaFileSourceTest.java index 45fc37ff..74300943 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaFileSourceTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaFileSourceTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java similarity index 99% rename from webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java rename to webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java index 84155f6b..da966af0 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaPlayerTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaReaderTest.java similarity index 99% rename from webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java rename to webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaReaderTest.java index e204fd77..4bc0477e 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/ffmpeg/MediaReaderTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaReaderTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.onvoid.webrtc.media.ffmpeg; +package dev.onvoid.webrtc.media.player; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; From 04de3046ae327aaba465c80940178bab9bd049cc Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 16:13:16 +0200 Subject: [PATCH 22/33] fix: let a player be closed from its own listener Listener calls arrive on the player's decode thread. Closing the player from one, which is the natural thing to do in onEndOfStream, released the native player on that same thread: it joined itself, which throws and takes the JVM down, and it deleted the player while the player was still on the stack below the call. MediaFileSource.close() did the same. A close from within a listener call now closes the player at once, so every later command does nothing and the state reads as closed, and leaves the release to another thread. That thread first waits for any command still running, since that command may be what made the call, and then releases the player without holding the lock, so the listener stays free to call the player while its thread is waited for. --- docs/guide/media/media-files.md | 2 + .../webrtc/media/player/MediaPlayer.java | 60 ++++++++++++++- .../media/player/MediaPlayerListener.java | 4 + .../media/player/MediaFileSourceTest.java | 23 ++++++ .../webrtc/media/player/MediaPlayerTest.java | 75 +++++++++++++++++++ 5 files changed, 160 insertions(+), 4 deletions(-) diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index 895a6c50..08fe3565 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -173,6 +173,8 @@ source.setListener(new MediaPlayerListener() { ::: warning Every call arrives on the player's own thread, and that thread is the one decoding the media. A listener must return promptly, and must not wait on the player. + +Closing the player, or the `MediaFileSource`, from a listener is fine: it takes effect at once, and the native player is released on another thread once the listener has returned. ::: ## Feeding Your Own Media Sources diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java index 4681f64f..def058fa 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java @@ -70,6 +70,13 @@ public class MediaPlayer implements AutoCloseable { /** Read on the native player's thread, so never a stale value. */ private volatile MediaPlayerListener listener; + /** + * How deep the current thread is in calls to the listener. Closing from + * in there cannot release the native player on the spot: the call came + * from the native player, which is still on the stack below it. + */ + private final ThreadLocal callbackDepth = ThreadLocal.withInitial(() -> new int[1]); + /** * Creates a player for the given source, delivering into the given custom @@ -186,6 +193,12 @@ public MediaPlayerState getState() { * Stops playback, releases the native player and the reader it took over, * and waits for the player's thread to finish. Closing a player that is * already closed does nothing. + *

+ * Closing from within a {@link MediaPlayerListener} call is allowed, but + * cannot wait: the player's thread is the one making that call. The player + * is closed at once, in that every later command does nothing and its + * state reads as closed, and it is released on another thread as soon as + * the listener call has returned. */ @Override public void close() { @@ -199,7 +212,31 @@ public void close() { handle = 0; } - dispose(closing); + if (closing == 0) { + return; + } + + if (callbackDepth.get()[0] == 0) { + dispose(closing); + + return; + } + + Thread releaser = new Thread(() -> { + // A command that was already running when the handle was cleared + // holds the lock, and may be what made the listener call that + // asked for this close. Taking the lock waits for it to return; + // releasing it before disposing keeps the listener free to call + // the player while the player's thread is being waited for. + synchronized (lock) { + // Nothing to do but wait. + } + + dispose(closing); + }, "MediaPlayer-close"); + + releaser.setDaemon(true); + releaser.start(); } /** Called by native code on the player's thread. */ @@ -207,7 +244,7 @@ private void onNativeStateChanged(int state) { MediaPlayerListener current = listener; if (current != null) { - current.onStateChanged(MediaPlayerState.of(state)); + notifyListener(() -> current.onStateChanged(MediaPlayerState.of(state))); } } @@ -216,7 +253,7 @@ private void onNativeEndOfStream() { MediaPlayerListener current = listener; if (current != null) { - current.onEndOfStream(); + notifyListener(current::onEndOfStream); } } @@ -225,7 +262,22 @@ private void onNativeError(String message) { MediaPlayerListener current = listener; if (current != null) { - current.onError(message); + notifyListener(() -> current.onError(message)); + } + } + + private void notifyListener(Runnable call) { + int[] depth = callbackDepth.get(); + + depth[0]++; + + try { + call.run(); + } + finally { + if (--depth[0] == 0) { + callbackDepth.remove(); + } } } diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java index 4e43611b..6d8cf1ce 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java @@ -23,6 +23,10 @@ * playback, and that thread is the one decoding the media. An implementation * must therefore return promptly and must not call back into the player in a * way that waits for it. + *

+ * Closing the player from here, or the {@link MediaFileSource} it belongs to, + * is fine: the native player is then released once this call has returned, + * rather than during it. * * @author Alex Andres */ diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaFileSourceTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaFileSourceTest.java index 74300943..4ac77d80 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaFileSourceTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaFileSourceTest.java @@ -139,6 +139,29 @@ void closesTwice() throws Exception { source.close(); } + @Test + void closesFromEndOfStream() throws Exception { + MediaFileSource source = new MediaFileSource(asset()); + CountDownLatch closed = new CountDownLatch(1); + + source.setListener(new MediaPlayerListener() { + + @Override + public void onEndOfStream() { + // Releases the sources while the player is still finishing + // on this very thread. + source.close(); + closed.countDown(); + } + }); + + source.seek(2_500_000); + source.play(); + + assertTrue(closed.await(10, TimeUnit.SECONDS), "not closed"); + assertEquals(MediaPlayerState.CLOSED, source.getState()); + } + private static Path asset() throws Exception { URL url = MediaFileSourceTest.class.getResource(ASSET); diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java index da966af0..92a9ea2e 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java @@ -303,6 +303,65 @@ void adoptedReaderIsClosed() throws Exception { } } + @Test + void closesFromEndOfStream() throws Exception { + try (Sources sources = new Sources()) { + MediaPlayer player = new MediaPlayer(new MediaReader(asset()), + sources.video, sources.audio); + CountDownLatch closed = new CountDownLatch(1); + + player.setListener(new MediaPlayerListener() { + + @Override + public void onEndOfStream() { + // On the player's own thread, which is the one closing it + // has to wait for. + player.close(); + closed.countDown(); + } + }); + + player.seek(2_500_000); + player.play(); + + assertTrue(closed.await(10, TimeUnit.SECONDS), "not closed"); + assertEquals(MediaPlayerState.CLOSED, player.getState()); + + player.close(); + } + } + + @Test + void closesFromStateChangeOfCommand() throws Exception { + try (Sources sources = new Sources()) { + MediaPlayer player = new MediaPlayer(new MediaReader(asset()), + sources.video, sources.audio); + AtomicInteger closes = new AtomicInteger(); + + player.setListener(new MediaPlayerListener() { + + @Override + public void onStateChanged(MediaPlayerState state) { + // Called from within play(), on the calling thread. + if (state == MediaPlayerState.PLAYING) { + player.close(); + closes.incrementAndGet(); + } + } + }); + + player.play(); + + assertEquals(1, closes.get()); + assertEquals(MediaPlayerState.CLOSED, player.getState()); + + // Commands after the close find nothing to act on. + player.play(); + player.seek(0); + player.close(); + } + } + private static Path asset() throws Exception { URL url = MediaPlayerTest.class.getResource(ASSET); @@ -311,6 +370,22 @@ private static Path asset() throws Exception { return Paths.get(url.toURI()); } + /** + * A pair of sources with no tracks, for tests that only need somewhere to + * deliver to. + */ + private static final class Sources implements AutoCloseable { + + final CustomVideoSource video = new CustomVideoSource(); + final CustomAudioSource audio = new CustomAudioSource(); + + @Override + public void close() { + video.dispose(); + audio.dispose(); + } + } + /** * One playback: the sources, the tracks made from them, the sinks that * count what arrives, and the player feeding it all. From 741d7bb79e7975c6c9c65485e0e1cdd399233576 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 16:13:24 +0200 Subject: [PATCH 23/33] fix: keep the sources a player feeds alive until it is closed A player pushes into its custom sources from a thread of its own, long after it was given their handles, but held no reference to them. Disposing of a source and its track while the player still ran freed the native source under the pacing thread, which then pushed into freed memory. The extension interface gains retain and release functions for both kinds of source, appended to the table as its versioning allows, so the version stays 1 and an extension tells from the size whether they are there. The pacer holds a reference to each source it feeds for as long as it exists, and the media module refuses a table too short to provide them. --- docs/guide/media/media-files.md | 2 + .../src/main/cpp/src/JNI_MediaPlayer.cpp | 9 +++++ .../src/main/cpp/src/media/MediaPacer.cpp | 18 +++++++++ .../webrtc/media/player/MediaPlayer.java | 4 ++ .../webrtc/media/player/MediaPlayerTest.java | 26 +++++++++++++ .../src/main/cpp/include/webrtc_java_api.h | 30 +++++++++++++++ .../src/main/cpp/src/api/ExtensionApi.cpp | 38 ++++++++++++++++++- 7 files changed, 126 insertions(+), 1 deletion(-) diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index 08fe3565..36118aee 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -199,6 +199,8 @@ player.play(); ::: info The player takes over the reader it is given. That reader must not be used or closed afterwards; closing the player releases it. + +It also keeps the native side of the media sources it feeds alive until it is closed, so disposing of a source or its track while the player runs is safe. ::: ## Closing diff --git a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp index 47afe7f7..9af6788c 100644 --- a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp +++ b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp @@ -86,6 +86,15 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create return 0; } + if (api->size < sizeof(webrtc_java_api)) { + // The same version, but from before the members this module relies + // on were appended. + ThrowIOException(env, "The loaded webrtc-java library provides " + + std::to_string(api->size) + " bytes of its interface, but this module needs " + + std::to_string(sizeof(webrtc_java_api))); + + return 0; + } auto player = std::make_unique(std::move(reader), api, reinterpret_cast(videoSourceHandle), diff --git a/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp b/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp index bcf131f4..fdb560a5 100644 --- a/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp +++ b/webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp @@ -69,11 +69,29 @@ namespace ffmpeg video_source_(video_source), audio_source_(audio_source) { + // The pacing thread pushes into the sources long after the call that + // handed them over, so they must not go away when the application + // disposes of them first. + if (video_source_ != nullptr) { + api_->video_source_retain(video_source_); + } + if (audio_source_ != nullptr) { + api_->audio_source_retain(audio_source_); + } } MediaPacer::~MediaPacer() { Stop(); + + // Nothing pushes any more. A frame WebRTC still holds does not need + // the source, only its release callback, which frees the frame alone. + if (video_source_ != nullptr) { + api_->video_source_release(video_source_); + } + if (audio_source_ != nullptr) { + api_->audio_source_release(audio_source_); + } } void MediaPacer::Start() diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java index def058fa..0af5ee2c 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayer.java @@ -36,6 +36,10 @@ * The player takes over the reader it is given. That reader must not be used * or closed afterwards; closing the player releases it. *

+ * The player also keeps the native side of the given sources alive until it + * is closed, so disposing of a source, or of a track made from it, while the + * player still runs is safe. + *

* Example: *

{@code
  * CustomVideoSource videoSource = new CustomVideoSource();
diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
index 92a9ea2e..764ddbad 100644
--- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
+++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
@@ -362,6 +362,32 @@ public void onStateChanged(MediaPlayerState state) {
 		}
 	}
 
+	@Test
+	void outlivesDisposedSources() throws Exception {
+		CustomVideoSource videoSource = new CustomVideoSource();
+		CustomAudioSource audioSource = new CustomAudioSource();
+		VideoTrack videoTrack = factory.createVideoTrack("video", videoSource);
+		AudioTrack audioTrack = factory.createAudioTrack("audio", audioSource);
+
+		try (MediaPlayer player = new MediaPlayer(new MediaReader(asset()),
+				videoSource, audioSource)) {
+			player.play();
+
+			Thread.sleep(300);
+
+			// Everything an application holds goes, the player carries on
+			// pushing into the sources it was given.
+			videoTrack.dispose();
+			audioTrack.dispose();
+			videoSource.dispose();
+			audioSource.dispose();
+
+			Thread.sleep(700);
+
+			assertEquals(MediaPlayerState.PLAYING, player.getState());
+		}
+	}
+
 	private static Path asset() throws Exception {
 		URL url = MediaPlayerTest.class.getResource(ASSET);
 
diff --git a/webrtc-jni/src/main/cpp/include/webrtc_java_api.h b/webrtc-jni/src/main/cpp/include/webrtc_java_api.h
index ddb7ece4..155c9739 100644
--- a/webrtc-jni/src/main/cpp/include/webrtc_java_api.h
+++ b/webrtc-jni/src/main/cpp/include/webrtc_java_api.h
@@ -186,6 +186,36 @@ struct webrtc_java_api {
 	 * @return WEBRTC_JAVA_OK or a negative error code.
 	 */
 	int (*audio_source_push)(void * source, const struct wj_audio_chunk * chunk);
+
+	/**
+	 * Takes a reference to a CustomVideoSource, which keeps it alive until
+	 * the matching video_source_release() however soon the application
+	 * disposes of it. An extension that keeps a handle beyond the call it was
+	 * given in, such as one pushing from a thread of its own, must hold one:
+	 * the handle is otherwise freed when the Java source is disposed.
+	 *
+	 * @param source The handle from NativeApi.handleOf(CustomVideoSource).
+	 */
+	void (*video_source_retain)(void * source);
+
+	/**
+	 * Drops a reference taken with video_source_retain(). The handle must
+	 * not be used afterwards.
+	 */
+	void (*video_source_release)(void * source);
+
+	/**
+	 * Takes a reference to a CustomAudioSource; see video_source_retain().
+	 *
+	 * @param source The handle from NativeApi.handleOf(CustomAudioSource).
+	 */
+	void (*audio_source_retain)(void * source);
+
+	/**
+	 * Drops a reference taken with audio_source_retain(). The handle must
+	 * not be used afterwards.
+	 */
+	void (*audio_source_release)(void * source);
 };
 
 #ifdef __cplusplus
diff --git a/webrtc-jni/src/main/cpp/src/api/ExtensionApi.cpp b/webrtc-jni/src/main/cpp/src/api/ExtensionApi.cpp
index a8d0f72e..e7bc858f 100644
--- a/webrtc-jni/src/main/cpp/src/api/ExtensionApi.cpp
+++ b/webrtc-jni/src/main/cpp/src/api/ExtensionApi.cpp
@@ -153,12 +153,48 @@ namespace jni
 			return WEBRTC_JAVA_OK;
 		}
 
+		// The handles are the pointers the Java sources hold, typed as the
+		// concrete class, which is why a video and an audio source each get
+		// their own pair: a void pointer cannot be released without knowing
+		// what it points to.
+		void ApiVideoSourceRetain(void * source)
+		{
+			if (source != nullptr) {
+				static_cast(source)->AddRef();
+			}
+		}
+
+		void ApiVideoSourceRelease(void * source)
+		{
+			if (source != nullptr) {
+				static_cast(source)->Release();
+			}
+		}
+
+		void ApiAudioSourceRetain(void * source)
+		{
+			if (source != nullptr) {
+				static_cast(source)->AddRef();
+			}
+		}
+
+		void ApiAudioSourceRelease(void * source)
+		{
+			if (source != nullptr) {
+				static_cast(source)->Release();
+			}
+		}
+
 		const webrtc_java_api kExtensionApi = {
 			WEBRTC_JAVA_API_VERSION,
 			static_cast(sizeof(webrtc_java_api)),
 			&ApiNowUs,
 			&ApiVideoSourcePush,
-			&ApiAudioSourcePush
+			&ApiAudioSourcePush,
+			&ApiVideoSourceRetain,
+			&ApiVideoSourceRelease,
+			&ApiAudioSourceRetain,
+			&ApiAudioSourceRelease
 		};
 	}
 

From bcd44e715a4ad76483477627e23b2776dacf19e9 Mon Sep 17 00:00:00 2001
From: Alex Andres 
Date: Sat, 26 Sep 2026 16:13:25 +0200
Subject: [PATCH 24/33] fix: report a player that fails to open its decoders

When the decoders failed to open, the IOException was thrown first and
the half made player released afterwards. Releasing it reported the
closed state to the Java observer, a call into Java with an exception
pending, and the observer then cleared what it took for an exception of
its own: the IOException. The constructor returned normally with a
player that did nothing.

The player is now released before the exception is thrown, with its
observer detached, since nothing listens yet. The observer also no
longer calls into Java at all while an exception is pending, so it can
neither make such a call nor swallow an exception that is not its own.
---
 .../cpp/include/media/JavaPlayerObserver.h    |  2 +
 .../src/main/cpp/src/JNI_MediaPlayer.cpp      |  7 ++
 .../main/cpp/src/media/JavaPlayerObserver.cpp | 15 +++-
 .../webrtc/media/player/MediaPlayerTest.java  | 18 +++++
 .../onvoid/webrtc/media/player/TestMedia.java | 80 +++++++++++++++++++
 5 files changed, 119 insertions(+), 3 deletions(-)
 create mode 100644 webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java

diff --git a/webrtc-java-media/src/main/cpp/include/media/JavaPlayerObserver.h b/webrtc-java-media/src/main/cpp/include/media/JavaPlayerObserver.h
index 187337b8..ef924f6d 100644
--- a/webrtc-java-media/src/main/cpp/include/media/JavaPlayerObserver.h
+++ b/webrtc-java-media/src/main/cpp/include/media/JavaPlayerObserver.h
@@ -54,6 +54,8 @@ namespace ffmpeg
 
 			// A listener that throws must not be left to surface somewhere
 			// unrelated later, so anything pending is reported and cleared.
+			bool CanCallJava(JNIEnv * env);
+
 			void ClearPendingException(JNIEnv * env);
 
 			JavaVM * vm_ = nullptr;
diff --git a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp
index 9af6788c..2753c57d 100644
--- a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp
+++ b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp
@@ -105,6 +105,13 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create
 	int result = player->Initialize();
 
 	if (result < 0) {
+		// Released before throwing: closing reports the closed state to the
+		// observer, which calls into Java, and a call into Java with an
+		// exception pending is not allowed, and would also clear it. Nothing
+		// is listening yet anyway, so the observer goes first.
+		player->SetObserver(nullptr);
+		player.reset();
+
 		ThrowIOException(env, "Opening the decoders failed: " + ErrorMessage(result));
 
 		return 0;
diff --git a/webrtc-java-media/src/main/cpp/src/media/JavaPlayerObserver.cpp b/webrtc-java-media/src/main/cpp/src/media/JavaPlayerObserver.cpp
index 02d8c160..57fbcd5c 100644
--- a/webrtc-java-media/src/main/cpp/src/media/JavaPlayerObserver.cpp
+++ b/webrtc-java-media/src/main/cpp/src/media/JavaPlayerObserver.cpp
@@ -72,7 +72,7 @@ namespace ffmpeg
 		bool attached = false;
 		JNIEnv * env = Attach(&attached);
 
-		if (env != nullptr) {
+		if (CanCallJava(env)) {
 			env->CallVoidMethod(player_, on_state_changed_, static_cast(state));
 
 			ClearPendingException(env);
@@ -90,7 +90,7 @@ namespace ffmpeg
 		bool attached = false;
 		JNIEnv * env = Attach(&attached);
 
-		if (env != nullptr) {
+		if (CanCallJava(env)) {
 			env->CallVoidMethod(player_, on_end_of_stream_);
 
 			ClearPendingException(env);
@@ -108,7 +108,7 @@ namespace ffmpeg
 		bool attached = false;
 		JNIEnv * env = Attach(&attached);
 
-		if (env != nullptr) {
+		if (CanCallJava(env)) {
 			jstring text = env->NewStringUTF(message.c_str());
 
 			if (text != nullptr) {
@@ -163,6 +163,15 @@ namespace ffmpeg
 		}
 	}
 
+	bool JavaPlayerObserver::CanCallJava(JNIEnv * env)
+	{
+		// A thread that is already carrying an exception is one returning
+		// from native code into Java, which is about to see it thrown. A call
+		// into Java now is not allowed, and clearing afterwards would swallow
+		// an exception that was never ours, so the event is dropped instead.
+		return env != nullptr && env->ExceptionCheck() == JNI_FALSE;
+	}
+
 	void JavaPlayerObserver::ClearPendingException(JNIEnv * env)
 	{
 		if (env->ExceptionCheck() == JNI_TRUE) {
diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
index 764ddbad..0e0306c1 100644
--- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
+++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
@@ -22,6 +22,7 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.IOException;
 import java.net.URL;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -48,6 +49,7 @@
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.api.parallel.Execution;
 import org.junit.jupiter.api.parallel.ExecutionMode;
 
@@ -81,6 +83,9 @@ class MediaPlayerTest {
 	private AudioDeviceModule audioModule;
 	private PeerConnectionFactory factory;
 
+	@TempDir
+	Path tempDir;
+
 
 	@BeforeAll
 	void initFactory() {
@@ -388,6 +393,19 @@ void outlivesDisposedSources() throws Exception {
 		}
 	}
 
+	@Test
+	void failsWithoutDecoder() throws Exception {
+		Path file = TestMedia.muLawWav(tempDir);
+
+		try (Sources sources = new Sources()) {
+			// The reader opens, since the demuxer knows the format.
+			MediaReader reader = new MediaReader(file);
+
+			assertThrows(IOException.class,
+					() -> new MediaPlayer(reader, null, sources.audio));
+		}
+	}
+
 	private static Path asset() throws Exception {
 		URL url = MediaPlayerTest.class.getResource(ASSET);
 
diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java
new file mode 100644
index 00000000..7656d913
--- /dev/null
+++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package dev.onvoid.webrtc.media.player;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Writes small media files that the committed assets cannot provide, because
+ * what makes them useful is something no encoder would produce on purpose.
+ * They are written by hand here rather than committed, so that what they hold
+ * is spelled out next to the tests that rely on it.
+ *
+ * @author Alex Andres
+ */
+final class TestMedia {
+
+	private TestMedia() {
+		// Static access only.
+	}
+
+	/**
+	 * Writes one second of 8 kHz G.711 mu-law in a WAV file. The WAV demuxer
+	 * is part of the FFmpeg build and knows the format, but its decoder is
+	 * not, so this opens as a reader and fails as a player.
+	 *
+	 * @param directory Where to write the file.
+	 *
+	 * @return The file written.
+	 */
+	static Path muLawWav(Path directory) throws IOException {
+		final int rate = 8000;
+		final int length = rate;
+
+		ByteBuffer buffer = ByteBuffer.allocate(44 + length)
+				.order(ByteOrder.LITTLE_ENDIAN);
+
+		buffer.put(ascii("RIFF")).putInt(36 + length).put(ascii("WAVE"));
+
+		buffer.put(ascii("fmt ")).putInt(16)
+				.putShort((short) 7)	// WAVE_FORMAT_MULAW
+				.putShort((short) 1)	// Channels
+				.putInt(rate)			// Sample rate
+				.putInt(rate)			// Byte rate
+				.putShort((short) 1)	// Block align
+				.putShort((short) 8);	// Bits per sample
+
+		buffer.put(ascii("data")).putInt(length);
+
+		while (buffer.hasRemaining()) {
+			// Silence in mu-law.
+			buffer.put((byte) 0xFF);
+		}
+
+		return Files.write(directory.resolve("mulaw.wav"), buffer.array());
+	}
+
+	private static byte[] ascii(String text) {
+		return text.getBytes(StandardCharsets.US_ASCII);
+	}
+
+}

From 727eda2a6a39b8a9ac126738a260a2b63fa6b921 Mon Sep 17 00:00:00 2001
From: Alex Andres 
Date: Sat, 26 Sep 2026 16:13:25 +0200
Subject: [PATCH 25/33] fix: follow a change of audio format in the middle of a
 stream

swresample was set up once, from the format the stream started with,
and every frame was handed to it as if it still had that format. A
stream that changes its channel layout, sample format or sample rate
along the way, as live streams and some files do, had its frames read
with the wrong layout: a stereo setup reading a mono frame reads past
its end, and splits its samples between the two channels.

The decoder now remembers the format swresample was set up for, and
sets it up again for the first frame that differs. The output keeps
its channel count, since the source it feeds expects one.
---
 .../src/main/cpp/include/media/AudioDecoder.h |  12 ++
 .../src/main/cpp/src/media/AudioDecoder.cpp   | 115 +++++++++++---
 .../webrtc/media/player/MediaPlayerTest.java  |  35 +++++
 .../onvoid/webrtc/media/player/TestMedia.java | 142 ++++++++++++++++++
 4 files changed, 284 insertions(+), 20 deletions(-)

diff --git a/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h b/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h
index 14a80c91..cb07a38a 100644
--- a/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h
+++ b/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h
@@ -82,12 +82,24 @@ namespace ffmpeg
 			// Resamples one decoded frame into the pending buffer.
 			int Resample(const AVFrame * frame);
 
+			// Sets swresample up to convert from the given input to the
+			// output this decoder produces, replacing any earlier setup.
+			int ConfigureResampler(const AVChannelLayout * layout, int format,
+					int sample_rate);
+
 			// Moves one chunk out of the pending buffer.
 			void TakeChunk(std::vector & chunk, int64_t * timestamp_us);
 
 			AVCodecContext * codec_context_ = nullptr;
 			SwrContext * swr_context_ = nullptr;
 			AVFrame * decoded_ = nullptr;
+
+			// The input swresample is set up for. A stream may change any of
+			// these from one frame to the next, and a frame that no longer
+			// matches would be read with the wrong layout.
+			AVChannelLayout in_layout_ = {};
+			int in_format_ = -1;
+			int in_rate_ = 0;
 			AVRational time_base_ = { 0, 1 };
 			int channels_ = 0;
 
diff --git a/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp b/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp
index c1594cac..0925d017 100644
--- a/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp
+++ b/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp
@@ -68,26 +68,13 @@ namespace ffmpeg
 
 		// WebRTC takes mono or stereo, so anything wider is downmixed. The
 		// downmix itself is swresample's job.
+		// The output keeps this channel count for the whole stream, even if
+		// the stream's own count changes later: the pending buffer and the
+		// source it feeds both assume one.
 		channels_ = codec_context_->ch_layout.nb_channels >= 2 ? 2 : 1;
 
-		AVChannelLayout out_layout;
-
-		av_channel_layout_default(&out_layout, channels_);
-
-		result = swr_alloc_set_opts2(&swr_context_,
-				&out_layout, AV_SAMPLE_FMT_S16, kSampleRate,
-				&codec_context_->ch_layout, codec_context_->sample_fmt,
-				codec_context_->sample_rate, 0, nullptr);
-
-		av_channel_layout_uninit(&out_layout);
-
-		if (result < 0) {
-			Close();
-
-			return result;
-		}
-
-		result = swr_init(swr_context_);
+		result = ConfigureResampler(&codec_context_->ch_layout,
+				codec_context_->sample_fmt, codec_context_->sample_rate);
 
 		if (result < 0) {
 			Close();
@@ -120,6 +107,10 @@ namespace ffmpeg
 			avcodec_free_context(&codec_context_);
 		}
 
+		av_channel_layout_uninit(&in_layout_);
+		in_format_ = -1;
+		in_rate_ = 0;
+
 		pending_.clear();
 
 		time_base_ = { 0, 1 };
@@ -201,11 +192,24 @@ namespace ffmpeg
 			have_timestamp_ = true;
 		}
 
+		if (frame->format != in_format_ || frame->sample_rate != in_rate_
+				|| av_channel_layout_compare(&frame->ch_layout, &in_layout_) != 0) {
+			// The stream changed its format. What swresample still holds of
+			// the old one is dropped with it, which is a few milliseconds at
+			// most, and far better than reading this frame as the old format.
+			int result = ConfigureResampler(&frame->ch_layout, frame->format,
+					frame->sample_rate);
+
+			if (result < 0) {
+				return result;
+			}
+		}
+
 		// swresample holds samples back when rates differ, and those come out
 		// of a later call, so the room needed is the delay plus this frame.
-		int64_t delay = swr_get_delay(swr_context_, codec_context_->sample_rate);
+		int64_t delay = swr_get_delay(swr_context_, in_rate_);
 		int64_t capacity = av_rescale_rnd(delay + frame->nb_samples, kSampleRate,
-				codec_context_->sample_rate, AV_ROUND_UP);
+				in_rate_, AV_ROUND_UP);
 
 		if (capacity <= 0) {
 			return 0;
@@ -232,6 +236,77 @@ namespace ffmpeg
 		return 0;
 	}
 
+	int AudioDecoder::ConfigureResampler(const AVChannelLayout * layout, int format,
+			int sample_rate)
+	{
+		if (layout == nullptr || layout->nb_channels <= 0 || sample_rate <= 0) {
+			return AVERROR(EINVAL);
+		}
+
+		if (swr_context_ != nullptr) {
+			swr_free(&swr_context_);
+		}
+
+		av_channel_layout_uninit(&in_layout_);
+		in_format_ = -1;
+		in_rate_ = 0;
+
+		// A layout that only gives a channel count leaves swresample nothing
+		// to build a downmix from, so it is read as the usual layout for that
+		// many channels. What is remembered is the frame's own layout, so the
+		// next frame compares equal to it.
+		AVChannelLayout mix_layout = {};
+		int result = 0;
+
+		if (layout->order == AV_CHANNEL_ORDER_UNSPEC) {
+			av_channel_layout_default(&mix_layout, layout->nb_channels);
+		}
+		else {
+			result = av_channel_layout_copy(&mix_layout, layout);
+		}
+
+		if (result < 0) {
+			return result;
+		}
+
+		AVChannelLayout out_layout = {};
+
+		av_channel_layout_default(&out_layout, channels_);
+
+		result = swr_alloc_set_opts2(&swr_context_,
+				&out_layout, AV_SAMPLE_FMT_S16, kSampleRate,
+				&mix_layout, static_cast(format), sample_rate,
+				0, nullptr);
+
+		av_channel_layout_uninit(&out_layout);
+		av_channel_layout_uninit(&mix_layout);
+
+		if (result < 0) {
+			return result;
+		}
+
+		result = swr_init(swr_context_);
+
+		if (result < 0) {
+			swr_free(&swr_context_);
+
+			return result;
+		}
+
+		result = av_channel_layout_copy(&in_layout_, layout);
+
+		if (result < 0) {
+			swr_free(&swr_context_);
+
+			return result;
+		}
+
+		in_format_ = format;
+		in_rate_ = sample_rate;
+
+		return 0;
+	}
+
 	void AudioDecoder::TakeChunk(std::vector & chunk, int64_t * timestamp_us)
 	{
 		const size_t wanted = static_cast(kFramesPerChunk) * channels_;
diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
index 0e0306c1..fb2bfa07 100644
--- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
+++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java
@@ -406,6 +406,36 @@ void failsWithoutDecoder() throws Exception {
 		}
 	}
 
+	@Test
+	void followsChannelChange() throws Exception {
+		Path file = TestMedia.channelSwitchingFlac(tempDir, 5, 10);
+
+		try (Playback playback = new Playback(new MediaReader(file))) {
+			playback.player.play();
+
+			assertTrue(playback.ended.await(10, TimeUnit.SECONDS), "no end of stream");
+
+			// Stereo out throughout, as the file started.
+			assertEquals(2, playback.channels.get());
+			assertTrue(Math.abs(playback.chunks.get() - 150) <= 2,
+					"audio chunks: " + playback.chunks.get());
+			assertEquals(0, playback.unevenChunks.get(),
+					"chunks with differing channels");
+		}
+	}
+
+	private static boolean channelsEqual(byte[] data) {
+		// Interleaved 16-bit frames: the left and right sample of a frame are
+		// the two bytes pairs of each four.
+		for (int i = 0; i + 3 < data.length; i += 4) {
+			if (data[i] != data[i + 2] || data[i + 1] != data[i + 3]) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
 	private static Path asset() throws Exception {
 		URL url = MediaPlayerTest.class.getResource(ASSET);
 
@@ -454,6 +484,7 @@ private final class Playback implements AutoCloseable {
 		final AtomicReference error = new AtomicReference<>();
 		final List frameTimes = Collections.synchronizedList(new ArrayList<>());
 		final AtomicLong firstChunkNs = new AtomicLong();
+		final AtomicInteger unevenChunks = new AtomicInteger();
 
 		private final VideoTrackSink videoSink = frame -> {
 			frameTimes.add(System.nanoTime());
@@ -468,6 +499,10 @@ private final class Playback implements AutoCloseable {
 			sampleRate.set(rate);
 			channels.set(ch);
 			framesPerChunk.set(count);
+
+			if (ch == 2 && !channelsEqual(data)) {
+				unevenChunks.incrementAndGet();
+			}
 		};
 
 		private boolean closed;
diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java
index 7656d913..26dc9201 100644
--- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java
+++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java
@@ -16,6 +16,7 @@
 
 package dev.onvoid.webrtc.media.player;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -33,6 +34,13 @@
  */
 final class TestMedia {
 
+	/** The rate of the FLAC file, which is what WebRTC takes unchanged. */
+	static final int FLAC_SAMPLE_RATE = 48000;
+
+	/** 100 ms per FLAC frame. */
+	static final int FLAC_BLOCK_SIZE = 4800;
+
+
 	private TestMedia() {
 		// Static access only.
 	}
@@ -73,6 +81,140 @@ static Path muLawWav(Path directory) throws IOException {
 		return Files.write(directory.resolve("mulaw.wav"), buffer.array());
 	}
 
+	/**
+	 * Writes a 48 kHz, 16-bit FLAC file that starts in stereo and switches to
+	 * mono part way through, which FLAC allows from one frame to the next.
+	 * 

+ * Every stereo frame carries the same signal in both channels, and every + * mono frame a sawtooth whose neighbouring samples always differ. Played + * correctly, as stereo output, the left and right channel are therefore + * equal throughout; a mono frame read as if it were still stereo splits + * its neighbouring samples between the two channels and makes them differ. + * + * @param directory Where to write the file. + * @param stereoFrames How many 100 ms frames of stereo to write first. + * @param monoFrames How many 100 ms frames of mono follow them. + * + * @return The file written. + */ + static Path channelSwitchingFlac(Path directory, int stereoFrames, + int monoFrames) throws IOException { + final int frameCount = stereoFrames + monoFrames; + + // The frame number is written as a single byte below. + if (frameCount > 127) { + throw new IllegalArgumentException("Too many frames: " + frameCount); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(ascii("fLaC")); + + // The last metadata block, of type STREAMINFO, 34 bytes long. + out.write(0x80); + writeInt(out, 34, 3); + + // Minimum and maximum block size, then unknown frame sizes. + writeInt(out, FLAC_BLOCK_SIZE, 2); + writeInt(out, FLAC_BLOCK_SIZE, 2); + writeInt(out, 0, 3); + writeInt(out, 0, 3); + + // Sample rate, channels - 1, bits per sample - 1, total samples. + long totalSamples = (long) frameCount * FLAC_BLOCK_SIZE; + long packed = ((long) FLAC_SAMPLE_RATE << 44) | (1L << 41) | (15L << 36) + | totalSamples; + + writeInt(out, packed, 8); + + // No MD5 of the audio. + out.write(new byte[16]); + + int sample = 0; + + for (int frame = 0; frame < frameCount; frame++) { + boolean stereo = frame < stereoFrames; + short[] samples = new short[FLAC_BLOCK_SIZE]; + + for (int i = 0; i < samples.length; i++) { + // A sawtooth in steps of 37, so that no two neighbouring + // samples are ever equal. + samples[i] = (short) ((sample++ * 37) % 16000 - 8000); + } + + out.write(flacFrame(frame, stereo ? 2 : 1, samples)); + } + + return Files.write(directory.resolve("channel-switch.flac"), out.toByteArray()); + } + + /** + * One FLAC frame of verbatim subframes, every channel carrying the given + * samples. + */ + private static byte[] flacFrame(int number, int channels, short[] samples) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + // Sync code, fixed block size. + out.write(0xFF); + out.write(0xF8); + // Block size as 16 bits at the end of the header; 48 kHz. + out.write(0x7A); + // Mono or independent stereo; 16 bits per sample. + out.write(((channels - 1) << 4) | 0x08); + // The frame number, which fits a single byte of its UTF-8 coding. + out.write(number); + writeInt(out, samples.length - 1, 2); + out.write(crc8(out.toByteArray())); + + for (int channel = 0; channel < channels; channel++) { + // A verbatim subframe, no wasted bits. + out.write(0x02); + + for (short value : samples) { + writeInt(out, value & 0xFFFF, 2); + } + } + + writeInt(out, crc16(out.toByteArray()), 2); + + return out.toByteArray(); + } + + private static void writeInt(ByteArrayOutputStream out, long value, int bytes) { + for (int shift = (bytes - 1) * 8; shift >= 0; shift -= 8) { + out.write((int) (value >>> shift) & 0xFF); + } + } + + private static int crc8(byte[] data) { + int crc = 0; + + for (byte b : data) { + crc ^= b & 0xFF; + + for (int bit = 0; bit < 8; bit++) { + crc = (crc & 0x80) != 0 ? ((crc << 1) ^ 0x07) & 0xFF : (crc << 1) & 0xFF; + } + } + + return crc; + } + + private static int crc16(byte[] data) { + int crc = 0; + + for (byte b : data) { + crc ^= (b & 0xFF) << 8; + + for (int bit = 0; bit < 8; bit++) { + crc = (crc & 0x8000) != 0 ? ((crc << 1) ^ 0x8005) & 0xFFFF : (crc << 1) & 0xFFFF; + } + } + + return crc; + } + private static byte[] ascii(String text) { return text.getBytes(StandardCharsets.US_ASCII); } From 794e1129542e773d0bfcf2950cdd0bbaad928f6a Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 16:28:20 +0200 Subject: [PATCH 26/33] fix: clean up the native libraries extracted on Windows A native library is extracted to a temporary file to be loaded. Windows keeps a loaded library locked until the process ends, so deleting it on exit never succeeds, and every run left a copy behind: the core library under a random name, and since the media module the FFmpeg libraries beside it. They add up to gigabytes. Each process now extracts into a directory of its own, under the libraries' own names. Where a loaded library can be deleted, as on Linux and macOS, the directory goes as soon as loading is done. On Windows the process holds a lock on a file in it until it ends, and the first load in every process sweeps away the directories whose lock is free. It waits until a directory is a minute old, so that it never sweeps one a process has only just created and not yet locked. The sweep also takes the randomly named copies earlier versions left behind; one still loaded somewhere cannot be deleted, and is left. Loading is synchronized as well, so that two classes initialized on different threads no longer extract and load the same library twice. --- .../onvoid/webrtc/internal/NativeLoader.java | 326 +++++++++++++----- .../webrtc/internal/NativeLoaderTests.java | 147 ++++++++ 2 files changed, 387 insertions(+), 86 deletions(-) create mode 100644 webrtc/src/test/java/dev/onvoid/webrtc/internal/NativeLoaderTests.java diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeLoader.java b/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeLoader.java index c7077c6e..43499ec7 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeLoader.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeLoader.java @@ -16,23 +16,80 @@ package dev.onvoid.webrtc.internal; -import java.io.File; +import java.io.IOException; import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Loads a native library from inside a JAR. + *

+ * A library has to be a file to be loaded, so it is extracted to a temporary + * directory first. Where the file system lets a loaded library be deleted, as + * on Linux and macOS, that directory is gone again as soon as everything in it + * is loaded. Windows does not: a loaded library stays locked until the process + * ends, so the process cannot clean up after itself, and deleting on exit + * never succeeds. There the process keeps one directory, marked as in use by a + * lock it holds until it ends, and each process sweeps away the directories of + * processes that have ended before it extracts anything of its own. * * @author Alex Andres */ public class NativeLoader { - private static final Set LOADED_LIB_SET = ConcurrentHashMap.newKeySet(); + /** What every extraction directory is named with. */ + static final String DIRECTORY_PREFIX = "webrtc-java-natives-"; + + /** The file whose lock marks an extraction directory as in use. */ + static final String LOCK_FILE_NAME = ".lock"; + + /** + * How old an extraction directory has to be before a sweep considers it. + * A process that has only just created its directory may not hold its + * lock yet, and must not lose the directory in the meantime. + */ + static final long SWEEP_MIN_AGE_MS = TimeUnit.MINUTES.toMillis(1); + + /** + * What earlier versions of this loader left behind on Windows: the + * library itself under a randomly numbered temporary file name. + */ + private static final Pattern LEGACY_FILE = + Pattern.compile("webrtc-java-windows-(x86_64|aarch64|aarch32)\\d+\\.dll"); + + private static final Set LOADED_LIB_SET = new HashSet<>(); + + /** Whether this process has swept up after earlier ones yet. */ + private static boolean swept; + + /** + * Where this process keeps its libraries, on a file system that cannot + * delete them while they are loaded. Created on first use. + */ + private static Path processDirectory; + + /** + * Held until the process ends, marking {@link #processDirectory} as in + * use. Kept reachable, since the lock goes with its channel. + */ + @SuppressWarnings("unused") + private static FileLock processLock; /** @@ -47,46 +104,7 @@ public class NativeLoader { * @see System#loadLibrary(String) */ public static void loadLibrary(final String libName) throws Exception { - if (LOADED_LIB_SET.contains(libName)) { - return; - } - - String osFamily = getOSFamily(); - String osArch = getOSArch(); - String libFileName = System.mapLibraryName(libName + "-" + osFamily + "-" + osArch); - String tempName = removeExtension(libFileName); - String ext = getExtension(libFileName); - - Path tempPath = Files.createTempFile(tempName, ext); - File tempFile = tempPath.toFile(); - - try (InputStream is = NativeLoader.class.getClassLoader().getResourceAsStream(libFileName)) { - Files.copy(is, tempPath, StandardCopyOption.REPLACE_EXISTING); - } - catch (Exception e) { - tempFile.delete(); - - throw e; - } - - try { - System.load(tempPath.toAbsolutePath().toString()); - - LOADED_LIB_SET.add(libName); - } - catch (Exception e) { - tempFile.delete(); - - throw e; - } - - if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { - // Assume POSIX compliant file system, library can be deleted after loading. - tempFile.delete(); - } - else { - tempFile.deleteOnExit(); - } + loadLibrary(libName, new String[0]); } /** @@ -108,30 +126,165 @@ public static void loadLibrary(final String libName) throws Exception { * * @throws Exception if one of the libraries could not be loaded. */ - public static void loadLibrary(final String libName, final String... dependencies) - throws Exception { + public static synchronized void loadLibrary(final String libName, + final String... dependencies) throws Exception { if (LOADED_LIB_SET.contains(libName)) { return; } + Path tempRoot = Paths.get(System.getProperty("java.io.tmpdir")); + + if (!swept) { + swept = true; + + sweep(tempRoot, System.currentTimeMillis()); + } + String libFileName = System.mapLibraryName( libName + "-" + getOSFamily() + "-" + getOSArch()); - Path tempDir = Files.createTempDirectory(libName); - tempDir.toFile().deleteOnExit(); + List fileNames = new ArrayList<>(); + + Collections.addAll(fileNames, dependencies); + fileNames.add(libFileName); - for (String dependency : dependencies) { - loadFromDirectory(tempDir, dependency); + if (canDeleteLoaded()) { + // A loaded library stays mapped after its file is gone, so the + // directory is only needed for as long as the loading takes. + Path directory = Files.createTempDirectory(tempRoot, DIRECTORY_PREFIX); + + try { + for (String fileName : fileNames) { + loadFromDirectory(directory, fileName); + } + } + finally { + deleteDirectory(directory); + } } + else { + Path directory = processDirectory(tempRoot); - loadFromDirectory(tempDir, libFileName); + for (String fileName : fileNames) { + loadFromDirectory(directory, fileName); + } + } LOADED_LIB_SET.add(libName); } + /** + * Deletes what earlier processes left behind in the given directory: + * extraction directories no process holds any more, and the library + * files earlier versions of this loader extracted. Anything that cannot + * be deleted is left, since on Windows that means a process still has it + * loaded. + * + * @param root The directory to sweep, which is the temporary directory + * outside of tests. + * @param now The current time in milliseconds, which is what the age of + * an entry is measured against. + */ + static void sweep(Path root, long now) { + try (DirectoryStream entries = Files.newDirectoryStream(root)) { + for (Path entry : entries) { + String name = entry.getFileName().toString(); + + try { + if (name.startsWith(DIRECTORY_PREFIX) && Files.isDirectory(entry)) { + if (isOldEnough(entry, now) && !isInUse(entry)) { + deleteDirectory(entry); + } + } + else if (LEGACY_FILE.matcher(name).matches()) { + if (isOldEnough(entry, now)) { + Files.deleteIfExists(entry); + } + } + } + catch (IOException | RuntimeException e) { + // In use, or already gone: either way not ours to delete. + } + } + } + catch (IOException e) { + // Nothing to sweep. + } + } + + /** + * Whether a process still holds the given extraction directory. A + * directory without a lock file is not held: it belongs to a process that + * ended before it could create one, since one that is still starting is + * too young to be asked. + * + * @param directory The extraction directory. + * + * @return True if a process, this one included, holds its lock. + * + * @throws IOException if the lock file cannot be opened. + */ + static boolean isInUse(Path directory) throws IOException { + Path lockFile = directory.resolve(LOCK_FILE_NAME); + + if (!Files.exists(lockFile)) { + return false; + } + + try (FileChannel channel = FileChannel.open(lockFile, StandardOpenOption.WRITE)) { + FileLock lock = channel.tryLock(); + + if (lock == null) { + // Another process holds it. + return true; + } + + lock.release(); + + return false; + } + catch (OverlappingFileLockException e) { + // This process holds it. + return true; + } + } + + /** + * Returns the directory of this process, and creates it, locked, the + * first time. + * + * @param tempRoot Where to create it. + * + * @return The directory of this process. + * + * @throws IOException if it cannot be created or locked. + */ + static synchronized Path processDirectory(Path tempRoot) throws IOException { + if (processDirectory == null) { + Path directory = Files.createTempDirectory(tempRoot, DIRECTORY_PREFIX); + FileChannel channel = FileChannel.open(directory.resolve(LOCK_FILE_NAME), + StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); + + try { + processLock = channel.lock(); + } + catch (IOException | RuntimeException e) { + channel.close(); + + throw e; + } + + processDirectory = directory; + } + + return processDirectory; + } + /** * Extracts one library from the JAR into the given directory, keeping its - * file name, and loads it. + * file name, and loads it. A library already extracted there under that + * name is loaded as it is: on Windows it is locked by having been loaded, + * and loading it again does nothing. * * @param directory The directory to extract into. * @param fileName The resource name of the library, which is also the @@ -144,60 +297,61 @@ private static void loadFromDirectory(Path directory, String fileName) throws Exception { Path libPath = directory.resolve(fileName); - try (InputStream is = NativeLoader.class.getClassLoader() - .getResourceAsStream(fileName)) { - if (is == null) { - throw new UnsatisfiedLinkError( - "Native library '" + fileName + "' is not on the classpath"); - } + if (!Files.exists(libPath)) { + try (InputStream is = NativeLoader.class.getClassLoader() + .getResourceAsStream(fileName)) { + if (is == null) { + throw new UnsatisfiedLinkError( + "Native library '" + fileName + "' is not on the classpath"); + } - Files.copy(is, libPath, StandardCopyOption.REPLACE_EXISTING); + Files.copy(is, libPath); + } } - File libFile = libPath.toFile(); - - libFile.deleteOnExit(); - try { System.load(libPath.toAbsolutePath().toString()); } catch (Throwable e) { - libFile.delete(); + Files.deleteIfExists(libPath); throw e; } } - private static String getExtension(String fileName) { - final int index = getExtensionIndex(fileName); - - if (index < 0) { - return ""; - } - - return fileName.substring(index); + private static boolean isOldEnough(Path path, long now) throws IOException { + return now - Files.getLastModifiedTime(path).toMillis() >= SWEEP_MIN_AGE_MS; } - private static String removeExtension(String fileName) { - final int index = getExtensionIndex(fileName); + /** + * Deletes the given directory and everything in it, as far as it can. + */ + private static void deleteDirectory(Path directory) { + List paths; - if (index < 0) { - return fileName; + try (Stream walk = Files.walk(directory)) { + paths = walk.collect(Collectors.toList()); + } + catch (IOException e) { + return; } - return fileName.substring(0, index); - } - - private static int getExtensionIndex(String fileName) { - final String file = fileName.replace("\\", "/"); - final int extSeparator = file.lastIndexOf("."); - final int pathSeparator = file.lastIndexOf("/"); + // Deepest first, so that every directory is empty by the time it is + // reached. + Collections.reverse(paths); - if (pathSeparator > extSeparator) { - return -1; + for (Path path : paths) { + try { + Files.deleteIfExists(path); + } + catch (IOException e) { + // Still loaded somewhere; a later sweep gets it. + } } + } - return extSeparator; + private static boolean canDeleteLoaded() { + return FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); } private static String getOSFamily() { diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/internal/NativeLoaderTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/internal/NativeLoaderTests.java new file mode 100644 index 00000000..43e16d0c --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/internal/NativeLoaderTests.java @@ -0,0 +1,147 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc.internal; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; +import java.util.stream.Stream; + +import dev.onvoid.webrtc.TestBase; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Covers the cleanup of extracted native libraries. What is swept is an + * isolated directory set up here, never the real temporary directory, and the + * time a sweep runs at is given rather than read from the clock. + * + * @author Alex Andres + */ +class NativeLoaderTests extends TestBase { + + /** Well past the age a sweep waits for. */ + private static final long LATER = NativeLoader.SWEEP_MIN_AGE_MS * 2; + + @TempDir + Path root; + + + @Test + void sweepsEndedProcess() throws Exception { + Path directory = extraction("ended", true); + + NativeLoader.sweep(root, now() + LATER); + + assertFalse(Files.exists(directory)); + } + + @Test + void sweepsUnlockedDirectory() throws Exception { + // A process that ended before it could create its lock file. + Path directory = extraction("unlocked", false); + + NativeLoader.sweep(root, now() + LATER); + + assertFalse(Files.exists(directory)); + } + + @Test + void keepsDirectoryInUse() throws Exception { + Path directory = extraction("in-use", true); + + try (FileChannel channel = FileChannel.open( + directory.resolve(NativeLoader.LOCK_FILE_NAME), StandardOpenOption.WRITE); + FileLock lock = channel.lock()) { + NativeLoader.sweep(root, now() + LATER); + + assertTrue(Files.exists(directory.resolve("library.dll"))); + } + } + + @Test + void keepsYoungDirectory() throws Exception { + // A process may not hold its lock yet right after creating it. + Path directory = extraction("young", false); + + NativeLoader.sweep(root, now()); + + assertTrue(Files.exists(directory)); + } + + @Test + void sweepsLegacyFiles() throws Exception { + Path legacy = Files.createFile(root.resolve("webrtc-java-windows-x86_641234567890.dll")); + Path unrelated = Files.createFile(root.resolve("webrtc-java-windows-x86_64.dll")); + + NativeLoader.sweep(root, now() + LATER); + + assertFalse(Files.exists(legacy)); + assertTrue(Files.exists(unrelated)); + } + + @Test + void holdsOwnDirectory() throws Exception { + // Elsewhere nothing of a loaded library is left to hold. + assumeFalse(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")); + + // Made by the load TestBase caused, so this only returns it. + Path directory = NativeLoader.processDirectory( + Paths.get(System.getProperty("java.io.tmpdir"))); + + try (Stream files = Files.list(directory)) { + // The library, next to the lock file. + assertTrue(files.count() > 1, "nothing extracted"); + } + + assertTrue(NativeLoader.isInUse(directory)); + } + + /** + * An extraction directory holding a library, made to look as old as a + * directory left behind by an ended process. + */ + private Path extraction(String name, boolean withLockFile) throws IOException { + Path directory = Files.createDirectory(root.resolve(NativeLoader.DIRECTORY_PREFIX + name)); + + Files.createFile(directory.resolve("library.dll")); + + if (withLockFile) { + Files.createFile(directory.resolve(NativeLoader.LOCK_FILE_NAME)); + } + + Files.setLastModifiedTime(directory, FileTime.fromMillis(now())); + + return directory; + } + + private static long now() { + return System.currentTimeMillis(); + } + +} From f980dfa1923db50b503f52bb85f125d6ee4ade60 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 16:28:29 +0200 Subject: [PATCH 27/33] fix: stop as paused when playback fails A failure to read or decode ended the player's thread, but left it reporting that it was playing, and every later play or seek did nothing, since there was no thread left to act on it. A failure now stops playback where it happened, as a pause does, and is reported as such before the error itself. The thread stays, so playing again carries on past what failed, and a seek can move away from it. The playing flag, the pacer and the recorded state now change together under the player's lock, since the failure is reported from the decode thread and could otherwise interleave with a play from another. The observer is still called only once the lock is released. --- .../src/main/cpp/include/media/MediaPlayer.h | 9 ++ .../src/main/cpp/src/media/MediaPlayer.cpp | 82 ++++++++++---- .../media/player/MediaPlayerListener.java | 5 +- .../webrtc/media/player/MediaPlayerState.java | 5 +- .../webrtc/media/player/MediaPlayerTest.java | 39 +++++++ .../onvoid/webrtc/media/player/TestMedia.java | 106 ++++++++++++++---- 6 files changed, 199 insertions(+), 47 deletions(-) diff --git a/webrtc-java-media/src/main/cpp/include/media/MediaPlayer.h b/webrtc-java-media/src/main/cpp/include/media/MediaPlayer.h index 43c8a742..c27eab85 100644 --- a/webrtc-java-media/src/main/cpp/include/media/MediaPlayer.h +++ b/webrtc-java-media/src/main/cpp/include/media/MediaPlayer.h @@ -114,6 +114,15 @@ namespace ffmpeg void PerformSeek(int64_t position_us); void SetState(int state); + + // Records the state, with the lock held. Returns whether it + // changed, in which case the caller reports it with NotifyState + // once the lock is released. + bool UpdateStateLocked(int state); + void NotifyState(int state); + + // Stops playback where it is, as a pause does, and reports the + // error. The thread stays, so that playback can be resumed. void ReportError(const std::string & message, int error); std::unique_ptr reader_; diff --git a/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp b/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp index 72695e87..4d564b19 100644 --- a/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp +++ b/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp @@ -103,6 +103,8 @@ namespace ffmpeg void MediaPlayer::Play() { + bool changed = false; + { std::lock_guard lock(mutex_); @@ -120,16 +122,23 @@ namespace ffmpeg seek_position_us_ = 0; } + // Under the lock, together with the flag: the decode thread stops + // playback on an error, and the two must not interleave. + pacer_->Resume(); + changed = UpdateStateLocked(kPlaying); + command_.notify_all(); } - pacer_->Resume(); - - SetState(kPlaying); + if (changed) { + NotifyState(kPlaying); + } } void MediaPlayer::Pause() { + bool changed = false; + { std::lock_guard lock(mutex_); @@ -138,11 +147,14 @@ namespace ffmpeg } playing_ = false; - } - pacer_->Pause(); + pacer_->Pause(); + changed = UpdateStateLocked(kPaused); + } - SetState(kPaused); + if (changed) { + NotifyState(kPaused); + } } void MediaPlayer::Seek(int64_t position_us) @@ -266,7 +278,10 @@ namespace ffmpeg bool end_of_stream = false; if (!PumpOnce(packet, &end_of_stream)) { - break; + // Playback has stopped and been reported. The thread stays, + // so that playing again carries on past what failed, and a + // seek can move away from it. + continue; } if (!end_of_stream) { @@ -477,39 +492,64 @@ namespace ffmpeg void MediaPlayer::SetState(int state) { - MediaPlayerObserver * observer = nullptr; + bool changed = false; { std::lock_guard lock(mutex_); - if (state_ == state) { - return; - } + changed = UpdateStateLocked(state); + } + + if (changed) { + NotifyState(state); + } + } - state_ = state; - observer = observer_.get(); + bool MediaPlayer::UpdateStateLocked(int state) + { + if (state_ == state) { + return false; } + state_ = state; + + return true; + } + + void MediaPlayer::NotifyState(int state) + { // Called with the lock released: an observer runs Java code, which - // must never happen underneath a lock of ours. - if (observer != nullptr) { - observer->OnStateChanged(state); + // must never happen underneath a lock of ours. The observer does not + // change once the thread runs, so it is read without the lock. + if (observer_ != nullptr) { + observer_->OnStateChanged(state); } } void MediaPlayer::ReportError(const std::string & message, int error) { - MediaPlayerObserver * observer = nullptr; + bool changed = false; { std::lock_guard lock(mutex_); - playing_ = false; - observer = observer_.get(); + // Playback stops where it failed, as if paused: what is queued + // stays queued, and playing again carries on from here. A player + // being closed is left to that. + if (!closing_ && playing_) { + playing_ = false; + + pacer_->Pause(); + changed = UpdateStateLocked(kPaused); + } + } + + if (changed) { + NotifyState(kPaused); } - if (observer != nullptr) { - observer->OnError(message + ": " + ErrorText(error)); + if (observer_ != nullptr) { + observer_->OnError(message + ": " + ErrorText(error)); } } } diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java index 6d8cf1ce..2a017e3a 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerListener.java @@ -48,7 +48,10 @@ default void onEndOfStream() { } /** - * Playback stopped because something went wrong. + * Playback stopped because something went wrong. The player is left + * {@link MediaPlayerState#PAUSED} where it failed: playing again carries + * on past what failed, and a seek moves away from it, though a source + * that cannot be read at all fails again. * * @param message What went wrong, as FFmpeg described it. */ diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerState.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerState.java index b197376d..26369140 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerState.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaPlayerState.java @@ -30,8 +30,9 @@ public enum MediaPlayerState { PLAYING, /** - * Playback is held where it is. Resuming carries on from there rather - * than delivering everything that fell due in the meantime. + * Playback is held where it is, because it was paused or because it + * failed. Resuming carries on from there rather than delivering + * everything that fell due in the meantime. */ PAUSED, diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java index fb2bfa07..1b0d6b21 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java @@ -424,6 +424,45 @@ void followsChannelChange() throws Exception { } } + @Test + void pausesOnDecodeError() throws Exception { + Path file = TestMedia.corruptFlac(tempDir, 10, 4); + + try (Playback playback = new Playback(new MediaReader(file))) { + CountDownLatch failed = new CountDownLatch(1); + CountDownLatch ended = new CountDownLatch(1); + + playback.player.setListener(new MediaPlayerListener() { + + @Override + public void onEndOfStream() { + ended.countDown(); + } + + @Override + public void onError(String message) { + failed.countDown(); + } + }); + + playback.player.play(); + + assertTrue(failed.await(10, TimeUnit.SECONDS), "no error"); + + // Stopped where it failed, rather than claiming to play on. + assertEquals(MediaPlayerState.PAUSED, playback.player.getState()); + + int before = playback.chunks.get(); + + // Carries on past the frame that failed. + playback.player.play(); + + assertTrue(ended.await(10, TimeUnit.SECONDS), "no end of stream"); + assertEquals(MediaPlayerState.ENDED, playback.player.getState()); + assertTrue(playback.chunks.get() > before, "nothing played after resuming"); + } + } + private static boolean channelsEqual(byte[] data) { // Interleaved 16-bit frames: the left and right sample of a frame are // the two bytes pairs of each four. diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java index 26dc9201..0bb637d1 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java @@ -23,6 +23,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.function.IntFunction; /** * Writes small media files that the committed assets cannot provide, because @@ -99,67 +101,109 @@ static Path muLawWav(Path directory) throws IOException { */ static Path channelSwitchingFlac(Path directory, int stereoFrames, int monoFrames) throws IOException { - final int frameCount = stereoFrames + monoFrames; + int[] channels = new int[stereoFrames + monoFrames]; + Arrays.fill(channels, 0, stereoFrames, 2); + Arrays.fill(channels, stereoFrames, channels.length, 1); + + return Files.write(directory.resolve("channel-switch.flac"), + flac(FLAC_SAMPLE_RATE, FLAC_BLOCK_SIZE, channels, TestMedia::sawtooth, -1)); + } + + /** + * Writes a 48 kHz, mono, 16-bit FLAC file in which one frame cannot be + * decoded: its header is intact, so the file reads as usual, but its + * subframe uses a coding type FLAC reserves, which the decoder rejects. + * + * @param directory Where to write the file. + * @param frames How many 100 ms frames to write. + * @param corruptFrame Which of them to spoil. + * + * @return The file written. + */ + static Path corruptFlac(Path directory, int frames, int corruptFrame) + throws IOException { + int[] channels = new int[frames]; + + Arrays.fill(channels, 1); + + return Files.write(directory.resolve("corrupt.flac"), + flac(FLAC_SAMPLE_RATE, FLAC_BLOCK_SIZE, channels, TestMedia::sawtooth, + corruptFrame)); + } + + /** + * A sawtooth in steps of 37, so that no two neighbouring samples are ever + * equal. + */ + private static short sawtooth(int n) { + return (short) ((n * 37) % 16000 - 8000); + } + + /** + * A FLAC stream of fixed size frames, as many as channel counts are given. + * The stream starts with the channel count of its first frame. + */ + private static byte[] flac(int sampleRate, int blockSize, int[] frameChannels, + IntFunction signal, int corruptFrame) { // The frame number is written as a single byte below. - if (frameCount > 127) { - throw new IllegalArgumentException("Too many frames: " + frameCount); + if (frameChannels.length > 127) { + throw new IllegalArgumentException("Too many frames: " + frameChannels.length); } ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(ascii("fLaC")); + writeBytes(out, ascii("fLaC")); // The last metadata block, of type STREAMINFO, 34 bytes long. out.write(0x80); writeInt(out, 34, 3); // Minimum and maximum block size, then unknown frame sizes. - writeInt(out, FLAC_BLOCK_SIZE, 2); - writeInt(out, FLAC_BLOCK_SIZE, 2); + writeInt(out, blockSize, 2); + writeInt(out, blockSize, 2); writeInt(out, 0, 3); writeInt(out, 0, 3); // Sample rate, channels - 1, bits per sample - 1, total samples. - long totalSamples = (long) frameCount * FLAC_BLOCK_SIZE; - long packed = ((long) FLAC_SAMPLE_RATE << 44) | (1L << 41) | (15L << 36) - | totalSamples; + long totalSamples = (long) frameChannels.length * blockSize; + long packed = ((long) sampleRate << 44) | ((long) (frameChannels[0] - 1) << 41) + | (15L << 36) | totalSamples; writeInt(out, packed, 8); // No MD5 of the audio. - out.write(new byte[16]); + writeBytes(out, new byte[16]); int sample = 0; - for (int frame = 0; frame < frameCount; frame++) { - boolean stereo = frame < stereoFrames; - short[] samples = new short[FLAC_BLOCK_SIZE]; + for (int frame = 0; frame < frameChannels.length; frame++) { + short[] samples = new short[blockSize]; for (int i = 0; i < samples.length; i++) { - // A sawtooth in steps of 37, so that no two neighbouring - // samples are ever equal. - samples[i] = (short) ((sample++ * 37) % 16000 - 8000); + samples[i] = signal.apply(sample++); } - out.write(flacFrame(frame, stereo ? 2 : 1, samples)); + writeBytes(out, flacFrame(frame, sampleRate, frameChannels[frame], samples, + frame == corruptFrame)); } - return Files.write(directory.resolve("channel-switch.flac"), out.toByteArray()); + return out.toByteArray(); } /** * One FLAC frame of verbatim subframes, every channel carrying the given * samples. */ - private static byte[] flacFrame(int number, int channels, short[] samples) { + private static byte[] flacFrame(int number, int sampleRate, int channels, + short[] samples, boolean corrupt) { ByteArrayOutputStream out = new ByteArrayOutputStream(); // Sync code, fixed block size. out.write(0xFF); out.write(0xF8); - // Block size as 16 bits at the end of the header; 48 kHz. - out.write(0x7A); + // Block size as 16 bits at the end of the header; the sample rate. + out.write(0x70 | sampleRateCode(sampleRate)); // Mono or independent stereo; 16 bits per sample. out.write(((channels - 1) << 4) | 0x08); // The frame number, which fits a single byte of its UTF-8 coding. @@ -168,8 +212,9 @@ private static byte[] flacFrame(int number, int channels, short[] samples) { out.write(crc8(out.toByteArray())); for (int channel = 0; channel < channels; channel++) { - // A verbatim subframe, no wasted bits. - out.write(0x02); + // A verbatim subframe, no wasted bits; or the first of the coding + // types FLAC reserves. + out.write(corrupt ? 0x04 : 0x02); for (short value : samples) { writeInt(out, value & 0xFFFF, 2); @@ -181,6 +226,21 @@ private static byte[] flacFrame(int number, int channels, short[] samples) { return out.toByteArray(); } + private static int sampleRateCode(int sampleRate) { + switch (sampleRate) { + case 44100: + return 0x9; + case 48000: + return 0xA; + default: + throw new IllegalArgumentException("Unsupported sample rate: " + sampleRate); + } + } + + private static void writeBytes(ByteArrayOutputStream out, byte[] bytes) { + out.write(bytes, 0, bytes.length); + } + private static void writeInt(ByteArrayOutputStream out, long value, int bytes) { for (int shift = (bytes - 1) * 8; shift >= 0; shift -= 8) { out.write((int) (value >>> shift) & 0xFF); From 72e81c17403d577b818da6053a08fd70bc62e4d3 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 16:28:30 +0200 Subject: [PATCH 28/33] fix: play the last few milliseconds of resampled audio swresample holds the last samples of a stream back, waiting for more input to filter them with. Nothing asked for them at the end, so audio that had to be resampled ended a few milliseconds short, and the gap was filled with silence. The decoder now drains swresample at the end of the stream, and before setting it up again for a change of format, where the samples it holds belong before the new frame. A seek or a loop resets it instead, since what it holds belongs to the position being left. --- .../src/main/cpp/include/media/AudioDecoder.h | 8 +++ .../src/main/cpp/src/media/AudioDecoder.cpp | 69 ++++++++++++++++++- .../webrtc/media/player/MediaPlayerTest.java | 30 ++++++++ .../onvoid/webrtc/media/player/TestMedia.java | 24 +++++++ 4 files changed, 128 insertions(+), 3 deletions(-) diff --git a/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h b/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h index cb07a38a..65a280fa 100644 --- a/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h +++ b/webrtc-java-media/src/main/cpp/include/media/AudioDecoder.h @@ -82,6 +82,10 @@ namespace ffmpeg // Resamples one decoded frame into the pending buffer. int Resample(const AVFrame * frame); + // Moves whatever swresample still holds into the pending buffer, + // which it only hands out when told that no more input follows. + int DrainResampler(); + // Sets swresample up to convert from the given input to the // output this decoder produces, replacing any earlier setup. int ConfigureResampler(const AVChannelLayout * layout, int format, @@ -112,6 +116,10 @@ namespace ffmpeg // packet timing looks like. int64_t next_timestamp_us_ = 0; bool have_timestamp_ = false; + + // Set once the resampler has been drained at the end of the + // stream, so that it is drained once and not on every call. + bool resampler_drained_ = false; }; } diff --git a/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp b/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp index 0925d017..ca1a9594 100644 --- a/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp +++ b/webrtc-java-media/src/main/cpp/src/media/AudioDecoder.cpp @@ -117,6 +117,7 @@ namespace ffmpeg channels_ = 0; next_timestamp_us_ = 0; have_timestamp_ = false; + resampler_drained_ = false; } void AudioDecoder::Flush() @@ -124,8 +125,15 @@ namespace ffmpeg if (codec_context_ != nullptr) { avcodec_flush_buffers(codec_context_); } + if (swr_context_ != nullptr) { + // What swresample holds back belongs to the old position as much + // as what is pending does. Initializing it again drops that and + // keeps its setup. + swr_init(swr_context_); + } pending_.clear(); + resampler_drained_ = false; // The next frame decoded tells where the audio now starts. have_timestamp_ = false; @@ -152,6 +160,20 @@ namespace ffmpeg int result = avcodec_receive_frame(codec_context_, decoded_); if (result == AVERROR_EOF) { + if (!resampler_drained_) { + // swresample holds the last few samples back, waiting for + // more input to filter them with. There is none, so they + // have to be asked for, or the stream ends short. + resampler_drained_ = true; + + result = DrainResampler(); + + if (result < 0) { + return result; + } + + continue; + } if (pending_.empty()) { return AVERROR_EOF; } @@ -195,9 +217,14 @@ namespace ffmpeg if (frame->format != in_format_ || frame->sample_rate != in_rate_ || av_channel_layout_compare(&frame->ch_layout, &in_layout_) != 0) { // The stream changed its format. What swresample still holds of - // the old one is dropped with it, which is a few milliseconds at - // most, and far better than reading this frame as the old format. - int result = ConfigureResampler(&frame->ch_layout, frame->format, + // the old one comes out first, since it plays before this frame. + int result = DrainResampler(); + + if (result < 0) { + return result; + } + + result = ConfigureResampler(&frame->ch_layout, frame->format, frame->sample_rate); if (result < 0) { @@ -236,6 +263,42 @@ namespace ffmpeg return 0; } + int AudioDecoder::DrainResampler() + { + if (swr_context_ == nullptr) { + return 0; + } + + for (;;) { + int capacity = swr_get_out_samples(swr_context_, 0); + + if (capacity <= 0) { + return 0; + } + + const size_t offset = pending_.size(); + + pending_.resize(offset + static_cast(capacity) * channels_); + + uint8_t * output = reinterpret_cast(pending_.data() + offset); + + // No input is what tells swresample to hand out what it holds. + int converted = swr_convert(swr_context_, &output, capacity, nullptr, 0); + + if (converted < 0) { + pending_.resize(offset); + + return converted; + } + + pending_.resize(offset + static_cast(converted) * channels_); + + if (converted == 0) { + return 0; + } + } + } + int AudioDecoder::ConfigureResampler(const AVChannelLayout * layout, int format, int sample_rate) { diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java index 1b0d6b21..0beb0156 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java @@ -463,6 +463,33 @@ public void onError(String message) { } } + @Test + void deliversResamplerTail() throws Exception { + // One second at 44.1 kHz, which is exactly 48000 samples at 48 kHz. + Path file = TestMedia.constantFlac(tempDir, 44100, 10, (short) 8000); + + try (Playback playback = new Playback(new MediaReader(file))) { + playback.player.play(); + + assertTrue(playback.ended.await(10, TimeUnit.SECONDS), "no end of stream"); + + assertEquals(100, playback.chunks.get()); + assertEquals(48000, playback.nonZeroSamples.get()); + } + } + + private static int countNonZero(byte[] data) { + int count = 0; + + for (int i = 0; i + 1 < data.length; i += 2) { + if (data[i] != 0 || data[i + 1] != 0) { + count++; + } + } + + return count; + } + private static boolean channelsEqual(byte[] data) { // Interleaved 16-bit frames: the left and right sample of a frame are // the two bytes pairs of each four. @@ -524,6 +551,7 @@ private final class Playback implements AutoCloseable { final List frameTimes = Collections.synchronizedList(new ArrayList<>()); final AtomicLong firstChunkNs = new AtomicLong(); final AtomicInteger unevenChunks = new AtomicInteger(); + final AtomicLong nonZeroSamples = new AtomicLong(); private final VideoTrackSink videoSink = frame -> { frameTimes.add(System.nanoTime()); @@ -542,6 +570,8 @@ private final class Playback implements AutoCloseable { if (ch == 2 && !channelsEqual(data)) { unevenChunks.incrementAndGet(); } + + nonZeroSamples.addAndGet(countNonZero(data)); }; private boolean closed; diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java index 0bb637d1..09b8c287 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestMedia.java @@ -110,6 +110,30 @@ static Path channelSwitchingFlac(Path directory, int stereoFrames, flac(FLAC_SAMPLE_RATE, FLAC_BLOCK_SIZE, channels, TestMedia::sawtooth, -1)); } + /** + * Writes a mono, 16-bit FLAC file holding a constant, non-zero value, at + * a rate other than the 48 kHz WebRTC takes, so that playing it means + * resampling it. Everything played from it is non-zero, apart from what + * is missing: resampled completely, it plays as exactly as many non-zero + * samples at 48 kHz as it lasts. + * + * @param directory Where to write the file. + * @param sampleRate 44100 or 48000. + * @param frames How many 100 ms frames to write. + * @param value The value of every sample. + * + * @return The file written. + */ + static Path constantFlac(Path directory, int sampleRate, int frames, + short value) throws IOException { + int[] channels = new int[frames]; + + Arrays.fill(channels, 1); + + return Files.write(directory.resolve("constant.flac"), + flac(sampleRate, sampleRate / 10, channels, n -> value, -1)); + } + /** * Writes a 48 kHz, mono, 16-bit FLAC file in which one frame cannot be * decoded: its header is intact, so the file reads as usual, but its From 0583d8cf3f4fd2c2ba52d25930624cfbac91b0fe Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 16:47:42 +0200 Subject: [PATCH 29/33] build: update FFmpeg to 8.1 The submodule moves to n8.1, and the libraries the module loads to the major versions that release carries: avutil 60, avcodec and avformat 62, swresample 6 and swscale 9. FFmpeg 8 removed libpostproc, and with it the configure option that turned it off. Everything else the build enables is still there under the same name. Updating showed that neither the install nor the jar ever lost a library. Rebuilding FFmpeg installed over the earlier release, and the jar's staging directory only ever gained files, so the first build shipped the 7.1.1 libraries beside the new ones. A rebuild now starts from an empty install directory, cleared only when the record of an earlier build of ours shows it is ours to clear, and the staging directory is filled afresh on every build. A test now checks that the loaded FFmpeg is the release the pom pins, and that it is an LGPL build. --- webrtc-java-media/pom.xml | 8 ++- webrtc-java-media/src/main/cpp/CMakeLists.txt | 5 ++ .../cpp/dependencies/ffmpeg/CMakeLists.txt | 17 ++++++- .../onvoid/webrtc/media/player/FFmpeg.java | 32 ++++++------ .../webrtc/media/player/FFmpegTest.java | 50 +++++++++++++++++++ webrtc-java-media/third-party/ffmpeg | 2 +- 6 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java diff --git a/webrtc-java-media/pom.xml b/webrtc-java-media/pom.xml index e348cad2..1ac5e969 100644 --- a/webrtc-java-media/pom.xml +++ b/webrtc-java-media/pom.xml @@ -18,7 +18,7 @@ - 7.1.1 + 8.1 @@ -72,6 +72,12 @@ ${project.build.directory}/natives + + + ${ffmpeg.version} + diff --git a/webrtc-java-media/src/main/cpp/CMakeLists.txt b/webrtc-java-media/src/main/cpp/CMakeLists.txt index e603fa37..85718327 100644 --- a/webrtc-java-media/src/main/cpp/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/CMakeLists.txt @@ -126,7 +126,12 @@ endif() # set(NATIVES_DIR "${CMAKE_CURRENT_BINARY_DIR}/../natives") +# The directory is filled afresh on every build, rather than added to: a +# library that is no longer part of the build, such as one of an earlier +# FFmpeg release under its older major version, must not stay in it and end +# up in the jar. The commands below run in the order they are declared. add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove_directory "${NATIVES_DIR}" COMMAND ${CMAKE_COMMAND} -E make_directory "${NATIVES_DIR}" COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${NATIVES_DIR}" 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 879e1eef..ff8e59ea 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -273,7 +273,6 @@ else() --disable-programs --disable-avdevice --disable-avfilter - --disable-postproc --disable-network --disable-everything --disable-autodetect @@ -412,7 +411,21 @@ else() # Build out of tree so the submodule working tree stays clean. set(FFMPEG_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/ffmpeg-build") - file(MAKE_DIRECTORY "${FFMPEG_BUILD_DIR}") + + # A rebuild starts from nothing. make install only adds to the install + # directory, so the libraries of an earlier FFmpeg would stay beside the + # new ones, under the older major version in their names, and be packaged + # into the jar with them. The install directory is only cleared when the + # record of an earlier build of ours shows it is ours to clear, since it + # can be pointed anywhere. The build directory always belongs to this + # build, and objects compiled from another release are no use to it. + if(EXISTS "${FFMPEG_COMPONENTS_FILE}") + message(STATUS "Clearing the earlier FFmpeg build in ${FFMPEG_INSTALL_DIR}") + file(REMOVE_RECURSE "${FFMPEG_INSTALL_DIR}") + endif() + + file(REMOVE_RECURSE "${FFMPEG_BUILD_DIR}") + file(MAKE_DIRECTORY "${FFMPEG_INSTALL_DIR}" "${FFMPEG_BUILD_DIR}") to_shell_path("${FFMPEG_BUILD_DIR}" FFMPEG_BUILD_DIR_SH) string(REPLACE ";" " " FFMPEG_CONFIGURE_ARGS_STR "${FFMPEG_CONFIGURE_ARGS}") diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/FFmpeg.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/FFmpeg.java index b738a4c5..0363221a 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/FFmpeg.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/FFmpeg.java @@ -51,7 +51,7 @@ private FFmpeg() { /** * Returns the FFmpeg version the loaded native library was built against, - * for example {@code 7.1.1}. Useful to confirm which FFmpeg an application + * for example {@code 8.1}. Useful to confirm which FFmpeg an application * actually ended up with, since the libraries may be replaced. * * @return The FFmpeg version string. @@ -88,29 +88,29 @@ private static String[] dependencies() { if (osName.startsWith("windows")) { return new String[] { - "avutil-59.dll", - "swresample-5.dll", - "swscale-8.dll", - "avcodec-61.dll", - "avformat-61.dll" + "avutil-60.dll", + "swresample-6.dll", + "swscale-9.dll", + "avcodec-62.dll", + "avformat-62.dll" }; } if (osName.startsWith("mac os")) { return new String[] { - "libavutil.59.dylib", - "libswresample.5.dylib", - "libswscale.8.dylib", - "libavcodec.61.dylib", - "libavformat.61.dylib" + "libavutil.60.dylib", + "libswresample.6.dylib", + "libswscale.9.dylib", + "libavcodec.62.dylib", + "libavformat.62.dylib" }; } return new String[] { - "libavutil.so.59", - "libswresample.so.5", - "libswscale.so.8", - "libavcodec.so.61", - "libavformat.so.61" + "libavutil.so.60", + "libswresample.so.6", + "libswscale.so.9", + "libavcodec.so.62", + "libavformat.so.62" }; } diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java new file mode 100644 index 00000000..229aaa0e --- /dev/null +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java @@ -0,0 +1,50 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc.media.player; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Tests which FFmpeg the module actually loads. + * + * @author Alex Andres + */ +class FFmpegTest { + + @Test + void loadsPinnedRelease() { + // Set by the build from the version the submodule is pinned to. + String expected = System.getProperty("ffmpeg.version"); + + assertNotNull(expected, "ffmpeg.version is not set"); + + // A release build names itself after its tag, n. + assertEquals("n" + expected, FFmpeg.version()); + } + + @Test + void isLgpl() { + // What the module ships under; a GPL or non-free build must not be + // what ended up in the jar. + assertTrue(FFmpeg.license().startsWith("LGPL"), FFmpeg.license()); + } + +} diff --git a/webrtc-java-media/third-party/ffmpeg b/webrtc-java-media/third-party/ffmpeg index db69d06e..9047fa1b 160000 --- a/webrtc-java-media/third-party/ffmpeg +++ b/webrtc-java-media/third-party/ffmpeg @@ -1 +1 @@ -Subproject commit db69d06eeeab4f46da15030a80d539efb4503ca8 +Subproject commit 9047fa1b084f76b1b4d065af2d743df1b40dfb56 From af9690f6afada2f442440bba8ffe22ba55cdd2fb Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 16:51:53 +0200 Subject: [PATCH 30/33] fix: give CI one ffmpeg.version to read CI reads the FFmpeg version out of the media module's pom by the name of its element, and the property that hands the version to the tests had that same name. The lookup found both, and wrote the second one, unexpanded, into the environment of every build, which rejected it before anything was built. The tests now get the version under a name of their own. --- webrtc-java-media/pom.xml | 6 ++++-- .../java/dev/onvoid/webrtc/media/player/FFmpegTest.java | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/webrtc-java-media/pom.xml b/webrtc-java-media/pom.xml index 1ac5e969..d2ba8c2c 100644 --- a/webrtc-java-media/pom.xml +++ b/webrtc-java-media/pom.xml @@ -74,9 +74,11 @@ + earlier one. Not named ffmpeg.version itself: CI reads + the version out of this file by that element name, and + has to find it exactly once. --> - ${ffmpeg.version} + ${ffmpeg.version} diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java index 229aaa0e..ed11fb9b 100644 --- a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/FFmpegTest.java @@ -32,9 +32,9 @@ class FFmpegTest { @Test void loadsPinnedRelease() { // Set by the build from the version the submodule is pinned to. - String expected = System.getProperty("ffmpeg.version"); + String expected = System.getProperty("test.ffmpeg.version"); - assertNotNull(expected, "ffmpeg.version is not set"); + assertNotNull(expected, "test.ffmpeg.version is not set"); // A release build names itself after its tag, n. assertEquals("n" + expected, FFmpeg.version()); From af7fceeff71f424dd6941ac91c2c3e4498140fa9 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 17:55:56 +0200 Subject: [PATCH 31/33] fix: name the FFmpeg build after its release, not the commit FFmpeg names its version after the git tag it finds in its source, and the shallow submodule checkout CI makes has no tags, so the libraries built there called themselves 9047fa1 rather than n8.1, and the test checking that the pinned release is loaded failed on every platform that runs it. The build now hands make the release the source says it is, read from its RELEASE file, so the name is the same however the source was checked out. It goes to make as REVISION: the makefile sets the revision version.sh reads from that variable, over anything in the environment. --- .../src/main/cpp/dependencies/ffmpeg/CMakeLists.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 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 ff8e59ea..0bad5d1e 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -468,9 +468,15 @@ fi "# Generated by CMake. Builds FFmpeg ${FFMPEG_VERSION} for webrtc-java-media. set -e ${FFMPEG_SHELL_PREAMBLE}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 +# source was checked out. It has to be REVISION on the make command line: the +# makefile sets the revision version.sh reads from it, over the environment. +FFMPEG_REVISION=\"n$(cat \"${FFMPEG_SOURCE_DIR_SH}/RELEASE\")\" \"${FFMPEG_SOURCE_DIR_SH}/configure\" ${FFMPEG_CONFIGURE_ARGS_STR} -\"${FFMPEG_MAKE}\" -j${BUILD_JOBS} -\"${FFMPEG_MAKE}\" install +\"${FFMPEG_MAKE}\" -j${BUILD_JOBS} REVISION=\"$FFMPEG_REVISION\" +\"${FFMPEG_MAKE}\" install REVISION=\"$FFMPEG_REVISION\" ") if(WIN32) From 5075f7ee2ebfdf5bb114c56795ffc32ea570ac0b Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 18:18:45 +0200 Subject: [PATCH 32/33] docs: advertise the media module The README gains a feature entry and a short section on sending media files: what the module does, a five line example with MediaFileSource, the formats it reads, and a link to the guide. The formats listed are the ones the FFmpeg build enables, and only files are named, since the build leaves networking out. The documentation home gets a feature card for it, and the guide overview now lists the Media Files guide, which only the sidebar did so far. The link to MediaFileExample's source pointed into a source directory that does not exist; it points to the example now. --- README.md | 2 ++ docs/guide/examples.md | 2 +- docs/guide/index.md | 1 + docs/index.md | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a93e40e..8a3a0e1e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The library provides a comprehensive set of Java classes that map to the WebRTC - **Cross-platform support** - Works on Windows, macOS, and Linux (x64, ARM, ARM64) - **Media capabilities** - Audio and video capture from cameras and microphones - **Desktop capture** - Screen and application window sharing +- **Media file playback** - Send video and audio files over a peer connection in place of a camera and microphone, with the optional FFmpeg-based `webrtc-java-media` module - **Data channels** - Bidirectional peer-to-peer data exchange - **Statistics API** - Detailed metrics for monitoring connection quality - **Simple integration** - Available as a Maven dependency @@ -28,6 +29,7 @@ For more detailed information, check out the documentation: - [Quickstart](https://jrtc.dev/guide/get-started) - Get up and running quickly with webrtc-java - [Guides](https://jrtc.dev/guide/) - Comprehensive documentation on using the library - [Examples](https://jrtc.dev/guide/examples) - Sample code demonstrating various features +- [Media Files](https://jrtc.dev/guide/media/media-files) - Sending video and audio files with the media module - [Build Notes](https://jrtc.dev/guide/build) - Instructions for building the library from source ## License diff --git a/docs/guide/examples.md b/docs/guide/examples.md index 2802c623..b3107960 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -54,7 +54,7 @@ This example is particularly useful for applications that need to implement scre ## Media File -The [`MediaFileExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/media/java/dev/onvoid/webrtc/examples/MediaFileExample.java) demonstrates how to send a media file over a peer connection, in place of a camera and a microphone. See the [Media Files](/guide/media/media-files) guide for the API it uses. +The [`MediaFileExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java) demonstrates how to send a media file over a peer connection, in place of a camera and a microphone. See the [Media Files](/guide/media/media-files) guide for the API it uses. **Key features demonstrated:** - Opening a media file with a `MediaFileSource` diff --git a/docs/guide/index.md b/docs/guide/index.md index 6e1b913e..b851497e 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -7,6 +7,7 @@ This section provides detailed guides for various features of the webrtc-java li - [Media Devices](/guide/media/media-devices) - Working with audio and video devices - [Bitrate and Framerate Constraints](/guide/media/constraints) - Controlling media quality - [Send-only and Receive-only](/guide/media/directionality) - Configure transceiver directions (send-only, receive-only or inactive) +- [Media Files](/guide/media/media-files) - Sending video and audio files instead of a camera and microphone ## Audio diff --git a/docs/index.md b/docs/index.md index 443f50ed..3e3d6dc6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,6 +26,8 @@ features: details: Thin JNI layer with minimal overhead, delivering near-native performance with minimal context switching between Java and native code. - title: Audio and video streaming details: Audio and video capture from cameras and microphones devices, with support for custom media sources for flexible streaming solutions. + - title: Media File Playback + details: Send video and audio files over a peer connection. The optional media module decodes with FFmpeg in native code and paces playback in real time, keeping audio and video in sync. - title: Screen Sharing details: Share application windows or the full desktop with minimal setup; integrate screen capture streams like any other media stream. - title: Data Channels From ff5dc6600c3f78e03ac7bdf3b8e008583a85bbfd Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 26 Sep 2026 21:15:37 +0200 Subject: [PATCH 33/33] feat: play RTSP streams, with timeouts and interruptible reads A source can now be a live stream behind an rtsp:// URL, such as an IP camera's, as well as a local file. The FFmpeg build enables networking, the RTSP demuxer and the RTP, UDP and TCP protocols. FFmpeg asks for RTP over UDP first and falls back to TCP, interleaved on the RTSP connection, when the server refuses UDP or nothing arrives. A network source can stall or vanish, so every call that waits on one now runs under a deadline, enforced through FFmpeg's interrupt callback: opening, probing, each read, seeking and closing. Running out of time reads as a timeout. The timeout is 10 seconds unless another is passed to MediaReader or MediaFileSource. Closing a player interrupts its reader first, so it never waits on a stalled stream, and the error that interruption causes is not reported to the listener. Every source is opened with a protocol whitelist. The RTSP demuxer brings the HTTP protocol along for tunnelling, and a URL passed on from a user must not be able to fetch through it. Testing against a server showed three more things to fix. An error opening a source quoted its URL, password included, and now leaves the credentials out. A looping player ignored a failed rewind, so a live stream that ended while looping reported a connection error; a source that cannot rewind now simply ends. And network errors read as bare numbers on Windows, where the C runtime has no words for them and FFmpeg passes most Winsock codes through; the common ones are now named, in a single helper that replaces three copies. The examples take stream URLs as well: the player shows a stream's URL without its credentials and disables looping for a live source, and both describe how to run them against a camera. The Media Files guide covers live streams, their timeouts and their limits. RtspStreamTest plays against a small RTSP server in the test sources, which refuses UDP and streams PCM audio over TCP. --- docs/guide/examples.md | 11 +- docs/guide/media/media-files.md | 26 +- .../webrtc/examples/MediaFileExample.java | 22 +- .../examples/MediaFilePlayerExample.java | 54 ++- .../cpp/dependencies/ffmpeg/CMakeLists.txt | 13 +- .../src/main/cpp/include/JNI_MediaReader.h | 4 +- .../src/main/cpp/include/media/ErrorText.h | 31 ++ .../src/main/cpp/include/media/MediaReader.h | 54 ++- .../src/main/cpp/src/JNI_MediaPlayer.cpp | 14 +- .../src/main/cpp/src/JNI_MediaReader.cpp | 32 +- .../src/main/cpp/src/media/ErrorText.cpp | 74 ++++ .../src/main/cpp/src/media/MediaPlayer.cpp | 39 +- .../src/main/cpp/src/media/MediaReader.cpp | 111 +++++- .../webrtc/media/player/MediaFileSource.java | 20 +- .../webrtc/media/player/MediaReader.java | 60 ++- .../webrtc/media/player/RtspStreamTest.java | 275 ++++++++++++++ .../webrtc/media/player/TestRtspServer.java | 350 ++++++++++++++++++ 17 files changed, 1100 insertions(+), 90 deletions(-) create mode 100644 webrtc-java-media/src/main/cpp/include/media/ErrorText.h create mode 100644 webrtc-java-media/src/main/cpp/src/media/ErrorText.cpp create mode 100644 webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/RtspStreamTest.java create mode 100644 webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestRtspServer.java diff --git a/docs/guide/examples.md b/docs/guide/examples.md index b3107960..a6636a84 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -57,25 +57,26 @@ This example is particularly useful for applications that need to implement scre The [`MediaFileExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java) demonstrates how to send a media file over a peer connection, in place of a camera and a microphone. See the [Media Files](/guide/media/media-files) guide for the API it uses. **Key features demonstrated:** -- Opening a media file with a `MediaFileSource` +- Opening a media file, or a live RTSP stream, with a `MediaFileSource` - Reading what the source contains from its `MediaInfo` - Creating audio and video tracks from the media sources it feeds - Adding those tracks to a peer connection - Following playback through a `MediaPlayerListener` -This example is useful for applications that stream pre-recorded media, or that need a dependable stand-in for a camera in testing. +This example is useful for applications that stream pre-recorded media, relay an IP camera into WebRTC, or need a dependable stand-in for a camera in testing. ::: 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 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=rtsp://camera.local/stream1" ``` ::: ## Media File Player -The [`MediaFilePlayerExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java) is a Swing application that sends a media file from one peer connection to another in the same process, then shows the received video and plays the received audio in sync. Start and Stop buttons set up and tear down the whole session. +The [`MediaFilePlayerExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java) is a Swing application that sends a media file, or a live RTSP stream, from one peer connection to another in the same process, then shows the received video and plays the received audio in sync. Start and Stop buttons set up and tear down the whole session. **Key features demonstrated:** - Connecting two peer connections in one application, without a signaling server @@ -83,11 +84,11 @@ The [`MediaFilePlayerExample`](https://github.com/devopvoid/webrtc-java/blob/mas - Keeping audio and video in sync by sending both tracks in the same media stream - Raising the video sender's `maxBitrate` above WebRTC's default, so a high-resolution file is not held to a fraction of its size - Rendering received video frames in a Swing component with `VideoBufferConverter` -- Showing the file's format, the playback position, and live receive metrics (codec, resolution, frame rate, bitrate, packet loss, jitter, audio level) read from `getStats()` +- Showing the source's format, the playback position, and live receive metrics (codec, resolution, frame rate, bitrate, packet loss, jitter, audio level) read from `getStats()` - Releasing senders, peer connections, tracks and the source in order on Stop ::: info -Like the [Media File](#media-file) example, this one needs the `webrtc-java-media` module. The file argument is optional; a file can also be chosen from the window. +Like the [Media File](#media-file) example, this one needs the `webrtc-java-media` module. The source argument is optional; a file can also be chosen from the window, or a stream URL typed into it. A stream URL's credentials are never shown, and the Loop box is disabled while a live stream plays, since it has no length to start over from. ```bash mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.MediaFilePlayerExample" -D"exec.args=movie.mp4" diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index 36118aee..c0fc63b8 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -7,6 +7,7 @@ This guide explains how to send a media file over a peer connection instead of a - Reading what a source contains with `MediaReader` and `MediaInfo` - Controlling playback and following it with a listener - Feeding your own media sources with `MediaPlayer` +- Playing a live stream from an IP camera or media server over RTSP Sending a file is a common need: a test pattern instead of a webcam, a pre-recorded briefing, a video that has to reach several participants. Without help, an application has to bring its own decoder and push I420 frames into a `CustomVideoSource` itself. The media module removes that work by decoding with [FFmpeg](https://ffmpeg.org) inside the library. @@ -203,6 +204,29 @@ The player takes over the reader it is given. That reader must not be used or cl It also keeps the native side of the media sources it feeds alive until it is closed, so disposing of a source or its track while the player runs is safe. ::: +## Playing a Live Stream + +A source can also be a live stream behind an `rtsp://` URL, which is what IP cameras, video recorders and most media servers offer. Everything above works the same way; only the URL differs: + +```java +MediaFileSource source = new MediaFileSource("rtsp://camera.local:554/stream1"); +``` + +Credentials go into the URL, as `rtsp://user:password@camera.local/stream1`. FFmpeg first asks the server for RTP over UDP, and falls back to RTP interleaved on the RTSP connection over TCP if the server refuses UDP or no UDP packets arrive, which is what gets a stream through most firewalls. + +A network source can stall or disappear, so every operation that waits on a source has a time limit: opening it, and then each read during playback. It is 10 seconds unless you pass another: + +```java +// Import required classes +import java.time.Duration; + +MediaFileSource source = new MediaFileSource("rtsp://camera.local/stream1", Duration.ofSeconds(5)); +``` + +An unreachable server makes the constructor throw an `IOException` once that time is up. A stream that stops sending during playback is reported to the listener's `onError`, and the player is left `PAUSED`. Closing a player never waits for the time limit: it breaks off whatever the player is waiting for at once. + +A live stream has no end and no length: its `MediaInfo` reports a duration of 0, looping does not apply, and seeking is reported as an error. Pausing stops reading from the server, but the server keeps sending; resuming carries on from where playback paused, which leaves it behind real time by as long as it was paused. To get back to live, close the source and open it again. + ## Closing Closing a `MediaFileSource` stops playback and releases the player along with both media sources. Release the senders a peer connection handed out, and the tracks, before that: @@ -227,7 +251,7 @@ The FFmpeg build is deliberately small, and carries only what this module plays: Audio of any rate or layout is resampled to what WebRTC takes, which is 48 kHz 16-bit PCM in mono or stereo. Video that decodes to I420 — almost all 8-bit H.264, VP8, VP9 and MPEG-4 — reaches the encoder without being copied; anything else is converted first. -Only local files play today. FFmpeg demuxes network sources just as well, so the same code will cover http, rtsp and rtmp once those protocols are turned on in the build. +Sources can be local files, or live streams over RTSP (`rtsp://`), with RTP over UDP or TCP. Protocols that need TLS, such as `rtsps://` and `https://`, are not part of the build, and neither are HLS, DASH and RTMP. A source cannot reach any other protocol either, even one FFmpeg uses internally, so a URL passed on from a user cannot make the module fetch something else. ## Licensing diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java index 7f8e62e0..ccc6a71d 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFileExample.java @@ -46,7 +46,7 @@ *

* This example shows how to: *

    - *
  • Open a media file or network stream with a MediaFileSource
  • + *
  • Open a media file, or a live RTSP stream, with a MediaFileSource
  • *
  • Read what the source contains from its MediaInfo
  • *
  • Create audio and video tracks from the sources it feeds
  • *
  • Add those tracks to a peer connection
  • @@ -61,14 +61,13 @@ * reach a remote peer through a signaling channel, as * {@link PeerConnectionExample} shows. *

    - * Run it with the media file to send: + * Run it with the media file or stream URL to send: *

      * java dev.onvoid.webrtc.examples.MediaFileExample movie.mp4
    + * java dev.onvoid.webrtc.examples.MediaFileExample rtsp://camera.local/stream1
      * 
    - *

    - * Only local files play today. FFmpeg demuxes network sources just as well, - * so the same code covers http, rtsp and rtmp once those protocols are turned - * on in the build. + * A stream that cannot be reached, or stops sending, is given up on after the + * reader's timeout, 10 seconds unless another is passed to the source. * * @author Alex Andres */ @@ -76,8 +75,9 @@ public class MediaFileExample { public static void main(String[] args) { if (args.length < 1) { - System.out.println("Usage: MediaFileExample "); + System.out.println("Usage: MediaFileExample "); System.out.println(" for example: MediaFileExample movie.mp4"); + System.out.println(" or: MediaFileExample rtsp://camera.local/stream1"); return; } @@ -144,7 +144,13 @@ public void onError(String message) { } private static void printInfo(MediaInfo info) { - System.out.printf("Source runs %.3f s%n", info.getDurationUs() / 1_000_000.0); + if (info.getDurationUs() > 0) { + System.out.printf("Source runs %.3f s%n", info.getDurationUs() / 1_000_000.0); + } + else { + // A live stream, which has no length. + System.out.println("Source is live"); + } if (info.hasVideo()) { System.out.printf(" video: %dx%d at %.2f fps, %s%n", info.getVideoWidth(), diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java index a0c30df8..5bd61875 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/MediaFilePlayerExample.java @@ -93,9 +93,10 @@ import dev.onvoid.webrtc.media.video.VideoTrackSink; /** - * Desktop UI counterpart to {@link MediaFileExample}: plays a media file - * through a pair of peer connections that live in the same application, and - * shows the received video while the received audio plays on the speakers. + * Desktop UI counterpart to {@link MediaFileExample}: plays a media file, or + * a live stream from an {@code rtsp://} URL such as an IP camera's, through a + * pair of peer connections that live in the same application, and shows the + * received video while the received audio plays on the speakers. *

    * This example shows how to: *

      @@ -119,10 +120,12 @@ * media stream: the receiver lines them up with each other before handing * them on, so the video sink only has to draw each frame as it arrives. *

      - * Run it with an optional media file to preselect: + * Run it with an optional media file or stream URL to preselect: *

        * java dev.onvoid.webrtc.examples.MediaFilePlayerExample movie.mp4
      + * java dev.onvoid.webrtc.examples.MediaFilePlayerExample rtsp://camera.local/stream1
        * 
      + * A live stream has no length, so the Loop box is disabled while one plays. * * @author Alex Andres */ @@ -155,7 +158,7 @@ public class MediaFilePlayerExample { return thread; }); - private final JFrame frame = new JFrame("Media File Player"); + private final JFrame frame = new JFrame("Media Player"); private final JTextField fileField = new JTextField(32); private final JButton browseButton = new JButton("Browse..."); private final JButton startButton = new JButton("Start"); @@ -182,6 +185,26 @@ public static void main(String[] args) { }); } + /** + * A short name for a source: a file's name, or a stream URL without the + * credentials it may carry. + */ + private static String sourceName(String source) { + return source.contains("://") ? withoutCredentials(source) : new File(source).getName(); + } + + /** + * The given source without the user name and password a URL can carry in + * front of its host, so that it can be shown. Anything else, a file path + * included, is returned as it is. + */ + private static String withoutCredentials(String source) { + // Everything between the scheme and the last @ before the path. Not + // parsed as a URI: a password is free to hold characters a URI + // parser rejects, and then it would be shown after all. + return source.replaceFirst("://[^/?#]*@", "://"); + } + /** * Makes the window look like the platform's own applications rather than * Swing's cross-platform default. It has to happen before the first @@ -207,6 +230,7 @@ private MediaFilePlayerExample() { private void show(String file) { fileField.setText(file); + fileField.setToolTipText("A media file, or an rtsp:// stream URL"); browseButton.addActionListener(e -> chooseFile()); startButton.addActionListener(e -> start()); @@ -270,7 +294,7 @@ private void chooseFile() { private void start() { String file = fileField.getText().trim(); if (file.isEmpty()) { - statusLabel.setText("Choose a media file first."); + statusLabel.setText("Choose a media file or enter an rtsp:// URL first."); return; } @@ -285,7 +309,12 @@ private void start() { Session started = new Session(file, looping); session = started; - SwingUtilities.invokeLater(() -> metricsPanel.showMedia(file, started.info)); + SwingUtilities.invokeLater(() -> { + metricsPanel.showMedia(file, started.info); + + // Only a source with a length can start over. + loopBox.setEnabled(started.info.getDurationUs() > 0); + }); } catch (Exception e) { LOG.log(Level.SEVERE, "Could not start playback", e); @@ -354,6 +383,10 @@ private void setRunning(boolean running) { fileField.setEnabled(!running); browseButton.setEnabled(!running); + if (!running) { + loopBox.setEnabled(true); + } + if (running) { statusTimer.start(); statsTimer.start(); @@ -906,7 +939,7 @@ private static class MetricsPanel extends JPanel { setPreferredSize(new Dimension(320, 0)); addSection("Media"); - addRow("media.file", "File"); + addRow("media.file", "Source"); addRow("media.duration", "Duration"); addRow("media.video", "Video"); addRow("media.audio", "Audio"); @@ -941,8 +974,9 @@ private static class MetricsPanel extends JPanel { } void showMedia(String file, MediaInfo info) { - set("media.file", new File(file).getName()); - values.get("media.file").setToolTipText(file); + set("media.file", sourceName(file)); + // Never the source as typed: a stream URL may carry a password. + values.get("media.file").setToolTipText(withoutCredentials(file)); set("media.duration", info.getDurationUs() > 0 ? formatTime(info.getDurationUs()) : "unknown"); set("media.video", info.hasVideo() 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 0bad5d1e..39f90a5d 100644 --- a/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt +++ b/webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt @@ -48,6 +48,11 @@ set(FFMPEG_COMPONENTS --enable-demuxer=mp3 --enable-demuxer=ogg --enable-demuxer=flac + # Live streams from IP cameras and media servers. The RTSP demuxer brings + # the RTP depacketizers along, and with them the demuxers of the formats + # RTP can carry (ASF, MOV, MPEG-TS, RealMedia), as well as the HTTP + # protocol for RTSP tunnelled over HTTP. + --enable-demuxer=rtsp --enable-demuxer=aac # Video this module decodes. --enable-decoder=h264 @@ -93,10 +98,13 @@ set(FFMPEG_COMPONENTS --enable-parser=opus --enable-parser=vorbis --enable-parser=flac - # Local files only for now; network protocols follow once the TLS - # backends are wired up per platform. + # Local files, and what RTSP runs over. Protocols that need TLS are left + # out until a TLS backend is chosen per platform. --enable-protocol=file --enable-protocol=pipe + --enable-protocol=rtp + --enable-protocol=udp + --enable-protocol=tcp ) # Written next to an install once it is built, naming the components it was @@ -273,7 +281,6 @@ else() --disable-programs --disable-avdevice --disable-avfilter - --disable-network --disable-everything --disable-autodetect ${FFMPEG_COMPONENTS} diff --git a/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h b/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h index 360ea570..f26d0b8a 100644 --- a/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h +++ b/webrtc-java-media/src/main/cpp/include/JNI_MediaReader.h @@ -25,10 +25,10 @@ extern "C" { /* * Class: dev_onvoid_webrtc_media_player_MediaReader * Method: open - * Signature: (Ljava/lang/String;)J + * Signature: (Ljava/lang/String;J)J */ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_open - (JNIEnv *, jclass, jstring); + (JNIEnv *, jclass, jstring, jlong); /* * Class: dev_onvoid_webrtc_media_player_MediaReader diff --git a/webrtc-java-media/src/main/cpp/include/media/ErrorText.h b/webrtc-java-media/src/main/cpp/include/media/ErrorText.h new file mode 100644 index 00000000..64af6d51 --- /dev/null +++ b/webrtc-java-media/src/main/cpp/include/media/ErrorText.h @@ -0,0 +1,31 @@ +/* + * 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. + */ + +#ifndef WEBRTC_JAVA_MEDIA_ERROR_TEXT_H_ +#define WEBRTC_JAVA_MEDIA_ERROR_TEXT_H_ + +#include + +namespace ffmpeg +{ + // What a negative AVERROR code means, in words, for messages that reach + // Java. FFmpeg describes most codes itself, but leaves plain errno codes + // to the C runtime, and on Windows that has no words for some of the + // ones networking produces, such as a timeout. + std::string ErrorText(int error); +} + +#endif diff --git a/webrtc-java-media/src/main/cpp/include/media/MediaReader.h b/webrtc-java-media/src/main/cpp/include/media/MediaReader.h index bf38a4c7..f51f2c49 100644 --- a/webrtc-java-media/src/main/cpp/include/media/MediaReader.h +++ b/webrtc-java-media/src/main/cpp/include/media/MediaReader.h @@ -17,6 +17,8 @@ #ifndef WEBRTC_JAVA_MEDIA_MEDIA_READER_H_ #define WEBRTC_JAVA_MEDIA_MEDIA_READER_H_ +#include +#include #include extern "C" { @@ -27,14 +29,22 @@ namespace ffmpeg { // Opens a media source with libavformat and reports what it contains. // - // A source is anything libavformat accepts: a file path today, and an - // http, rtsp or rtmp URL once those protocols are enabled in the build. - // Opening reads the container and picks the streams that will be played. - // Decoding is built on top of this and does not belong here. + // A source is a file path or an rtsp:// URL. Opening reads the container + // and picks the streams that will be played. Decoding is built on top of + // this and does not belong here. + // + // Every call that can block on the source, which for a network source is + // every call that touches it, gives up once the timeout has passed, and + // Interrupt() makes one give up at once from another thread. class MediaReader { public: - MediaReader() = default; + // How long a single blocking call may take when no timeout is + // given: opening, reading a packet, seeking or closing. + static constexpr int64_t kDefaultTimeoutUs = 10 * 1000 * 1000; + + // A timeout of 0 or less means no timeout. + explicit MediaReader(int64_t timeout_us = kDefaultTimeoutUs); ~MediaReader(); MediaReader(const MediaReader &) = delete; @@ -43,8 +53,14 @@ namespace ffmpeg // Opens the given source and selects the video and audio stream // that are meant to be played. Returns 0, or the negative AVERROR // libavformat reported, in which case the reader stays closed. + // Running out of time reads as AVERROR(ETIMEDOUT). int Open(const std::string & url); + // Makes the call blocking on the source return at once, and + // every later one too. May be called from any thread; the reader + // can only be closed afterwards. + void Interrupt(); + // Releases the container. Does nothing on a closed reader, and // runs from the destructor. void Close(); @@ -83,7 +99,8 @@ namespace ffmpeg // Reads the next packet of any stream into the given packet, which // the caller unrefs. Returns 0, AVERROR_EOF once the source is - // exhausted, or another negative AVERROR. + // exhausted, AVERROR(ETIMEDOUT) if nothing arrived in time, + // AVERROR_EXIT once interrupted, or another negative AVERROR. int ReadPacket(AVPacket * packet); // Moves to the keyframe at or before the given position, in @@ -92,9 +109,34 @@ namespace ffmpeg int Seek(int64_t position_us); private: + // Polled by libavformat while it blocks. Returning non-zero + // makes the blocking call fail with AVERROR_EXIT. + static int OnInterrupt(void * opaque); + + // Starts the time the next blocking call has, and ends it. + void BeginBlocking(); + void EndBlocking(); + + // A blocking call that failed because it ran out of time rather + // than because it was interrupted is reported as a timeout. + int TranslateError(int error) const; + AVFormatContext * format_context_ = nullptr; int video_stream_index_ = -1; int audio_stream_index_ = -1; + + const int64_t timeout_us_; + + // When the current blocking call runs out of time, on the clock + // of av_gettime_relative(), or 0 while none is running. + std::atomic deadline_us_{ 0 }; + std::atomic interrupted_{ false }; + + // Set when the callback ended the current blocking call because + // its time was up. libavformat does not always hand back the + // AVERROR_EXIT the callback causes; an aborted RTSP exchange, for + // one, can come back as an I/O error. + std::atomic timed_out_{ false }; }; } diff --git a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp index 2753c57d..3120128b 100644 --- a/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp +++ b/webrtc-java-media/src/main/cpp/src/JNI_MediaPlayer.cpp @@ -15,6 +15,7 @@ */ #include "JNI_MediaPlayer.h" +#include "media/ErrorText.h" #include "media/JavaPlayerObserver.h" #include "media/MediaPlayer.h" #include "media/MediaReader.h" @@ -29,17 +30,6 @@ extern "C" { namespace { - std::string ErrorMessage(int error) - { - char buffer[AV_ERROR_MAX_STRING_SIZE] = { 0 }; - - if (av_strerror(error, buffer, sizeof(buffer)) < 0) { - return "Unknown FFmpeg error " + std::to_string(error); - } - - return buffer; - } - void ThrowIOException(JNIEnv * env, const std::string & message) { jclass cls = env->FindClass("java/io/IOException"); @@ -112,7 +102,7 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create player->SetObserver(nullptr); player.reset(); - ThrowIOException(env, "Opening the decoders failed: " + ErrorMessage(result)); + ThrowIOException(env, "Opening the decoders failed: " + ffmpeg::ErrorText(result)); return 0; } diff --git a/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp b/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp index ee0bba0d..07781a6f 100644 --- a/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp +++ b/webrtc-java-media/src/main/cpp/src/JNI_MediaReader.cpp @@ -15,6 +15,7 @@ */ #include "JNI_MediaReader.h" +#include "media/ErrorText.h" #include "media/MediaReader.h" #include @@ -25,18 +26,27 @@ extern "C" { namespace { - // Turns an AVERROR code into the message FFmpeg has for it, so that a - // failure to open reaches Java saying what libavformat actually objected - // to instead of a bare number. - std::string ErrorMessage(int error) + // The source as it may be shown: a URL loses the user name and password + // it carries in front of its host, since error messages end up in logs. + // Anything that is not a URL with credentials is returned as it is. + std::string WithoutCredentials(const std::string & source) { - char buffer[AV_ERROR_MAX_STRING_SIZE] = { 0 }; + const size_t scheme_end = source.find("://"); - if (av_strerror(error, buffer, sizeof(buffer)) < 0) { - return "Unknown FFmpeg error " + std::to_string(error); + if (scheme_end == std::string::npos) { + return source; } - return buffer; + const size_t authority_start = scheme_end + 3; + const size_t authority_end = source.find_first_of("/?#", authority_start); + const size_t at = source.rfind('@', authority_end == std::string::npos + ? std::string::npos : authority_end - 1); + + if (at == std::string::npos || at < authority_start) { + return source; + } + + return source.substr(0, authority_start) + source.substr(at + 1); } void ThrowIOException(JNIEnv * env, const std::string & message) @@ -67,7 +77,7 @@ namespace } JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_open -(JNIEnv * env, jclass caller, jstring source) +(JNIEnv * env, jclass caller, jstring source, jlong timeoutUs) { if (source == nullptr) { ThrowIOException(env, "Source must not be null"); @@ -86,13 +96,13 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_open env->ReleaseStringUTFChars(source, chars); - auto reader = new ffmpeg::MediaReader(); + auto reader = new ffmpeg::MediaReader(static_cast(timeoutUs)); int result = reader->Open(url); if (result < 0) { delete reader; - ThrowIOException(env, "Opening '" + url + "' failed: " + ErrorMessage(result)); + ThrowIOException(env, "Opening '" + WithoutCredentials(url) + "' failed: " + ffmpeg::ErrorText(result)); return 0; } diff --git a/webrtc-java-media/src/main/cpp/src/media/ErrorText.cpp b/webrtc-java-media/src/main/cpp/src/media/ErrorText.cpp new file mode 100644 index 00000000..31ab51d1 --- /dev/null +++ b/webrtc-java-media/src/main/cpp/src/media/ErrorText.cpp @@ -0,0 +1,74 @@ +/* + * 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. + */ + +#include "media/ErrorText.h" + +#include + +extern "C" { +#include +} + +namespace +{ + struct NamedError + { + int error; + const char * text; + }; + + // Spelled out rather than left to av_strerror, which hands plain errno + // codes to the C runtime: its words for these differ between platforms, + // and MSVC's has none at all for the networking ones. + const NamedError kNamedErrors[] = { + { AVERROR(ETIMEDOUT), "Timed out" }, + { AVERROR(ECONNREFUSED), "Connection refused" }, + { AVERROR(ECONNRESET), "Connection reset" }, + { AVERROR(ECONNABORTED), "Connection aborted" }, + { AVERROR(EHOSTUNREACH), "Host unreachable" }, + { AVERROR(ENETUNREACH), "Network unreachable" }, +#ifdef _WIN32 + // FFmpeg maps only a few Winsock errors to errno codes, and hands + // the others on as the negated Winsock code. These are the ones a + // dropped or unreachable stream produces. + { -10050, "Network is down" }, // WSAENETDOWN + { -10051, "Network unreachable" }, // WSAENETUNREACH + { -10053, "Connection aborted" }, // WSAECONNABORTED + { -10054, "Connection reset" }, // WSAECONNRESET + { -10065, "Host unreachable" }, // WSAEHOSTUNREACH +#endif + }; +} + +namespace ffmpeg +{ + std::string ErrorText(int error) + { + for (const NamedError & named : kNamedErrors) { + if (named.error == error) { + return named.text; + } + } + + char buffer[AV_ERROR_MAX_STRING_SIZE] = { 0 }; + + if (av_strerror(error, buffer, sizeof(buffer)) < 0) { + return "FFmpeg error " + std::to_string(error); + } + + return buffer; + } +} diff --git a/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp b/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp index 4d564b19..3e007847 100644 --- a/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp +++ b/webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp @@ -15,6 +15,7 @@ */ #include "media/MediaPlayer.h" +#include "media/ErrorText.h" #include #include @@ -23,20 +24,6 @@ extern "C" { #include } -namespace -{ - std::string ErrorText(int error) - { - char buffer[AV_ERROR_MAX_STRING_SIZE] = { 0 }; - - if (av_strerror(error, buffer, sizeof(buffer)) < 0) { - return "error " + std::to_string(error); - } - - return buffer; - } -} - namespace ffmpeg { MediaPlayer::MediaPlayer(std::unique_ptr reader, @@ -205,6 +192,13 @@ namespace ffmpeg command_.notify_all(); } + // A decode thread blocked reading a source that has gone quiet, such + // as a stalled network stream, would otherwise hold up the join below + // until the read timed out. + if (reader_ != nullptr) { + reader_->Interrupt(); + } + // Stopping the pacer is what releases a decode thread waiting for room // in a queue that nothing is draining any more. pacer_->Stop(); @@ -292,7 +286,9 @@ namespace ffmpeg // goes out before anything is rewound. DrainDecoders(); - if (looping_.load()) { + // A source that cannot be rewound, such as a live stream that has + // ended, cannot start over either, and ends as if not looping. + if (looping_.load() && reader_->Seek(0) >= 0) { int64_t advance = reader_->GetDurationUs(); if (advance <= 0) { @@ -303,7 +299,6 @@ namespace ffmpeg loop_offset_us_.fetch_add(advance); - reader_->Seek(0); video_decoder_.Flush(); audio_decoder_.Flush(); @@ -533,10 +528,16 @@ namespace ffmpeg { std::lock_guard lock(mutex_); + // A player being closed fails whatever it was waiting on on + // purpose, since closing interrupts the reader. That is not an + // error of the source, and nobody should hear of it. + if (closing_) { + return; + } + // Playback stops where it failed, as if paused: what is queued - // stays queued, and playing again carries on from here. A player - // being closed is left to that. - if (!closing_ && playing_) { + // stays queued, and playing again carries on from here. + if (playing_) { playing_ = false; pacer_->Pause(); diff --git a/webrtc-java-media/src/main/cpp/src/media/MediaReader.cpp b/webrtc-java-media/src/main/cpp/src/media/MediaReader.cpp index 32493604..1c0eb001 100644 --- a/webrtc-java-media/src/main/cpp/src/media/MediaReader.cpp +++ b/webrtc-java-media/src/main/cpp/src/media/MediaReader.cpp @@ -16,13 +16,32 @@ #include "media/MediaReader.h" +#include + extern "C" { #include +#include #include } +namespace +{ + // The protocols a source may use, including the ones a source opens on + // its own behalf, such as the TCP connection behind an rtsp:// URL. The + // build also carries HTTP, for RTSP tunnelled over it, which is left out + // here: nothing this module plays is meant to fetch an arbitrary URL. + constexpr const char * kProtocolWhitelist = "file,pipe,rtsp,rtp,udp,tcp"; + + std::once_flag network_initialized; +} + namespace ffmpeg { + MediaReader::MediaReader(int64_t timeout_us) + : timeout_us_(timeout_us) + { + } + MediaReader::~MediaReader() { Close(); @@ -32,19 +51,46 @@ namespace ffmpeg { Close(); - int result = avformat_open_input(&format_context_, url.c_str(), nullptr, nullptr); + std::call_once(network_initialized, [] { + avformat_network_init(); + }); + + // Allocated here rather than by avformat_open_input, since the + // interrupt callback has to be in place before opening starts. + format_context_ = avformat_alloc_context(); + + if (format_context_ == nullptr) { + return AVERROR(ENOMEM); + } + + format_context_->interrupt_callback.callback = &MediaReader::OnInterrupt; + format_context_->interrupt_callback.opaque = this; + + AVDictionary * options = nullptr; + + av_dict_set(&options, "protocol_whitelist", kProtocolWhitelist, 0); + + BeginBlocking(); + int result = avformat_open_input(&format_context_, url.c_str(), nullptr, &options); + EndBlocking(); + + av_dict_free(&options); if (result < 0) { // avformat_open_input frees the context and nulls the pointer // itself when it fails, so there is nothing left to release. format_context_ = nullptr; - return result; + return TranslateError(result); } + BeginBlocking(); result = avformat_find_stream_info(format_context_, nullptr); + EndBlocking(); if (result < 0) { + result = TranslateError(result); + Close(); return result; @@ -80,7 +126,12 @@ namespace ffmpeg void MediaReader::Close() { if (format_context_ != nullptr) { + // Closing an RTSP source tells the server, which can block as + // well, unless the reader was interrupted, in which case it + // gives up at once. + BeginBlocking(); avformat_close_input(&format_context_); + EndBlocking(); } format_context_ = nullptr; @@ -185,7 +236,11 @@ namespace ffmpeg return AVERROR(EINVAL); } - return av_read_frame(format_context_, packet); + BeginBlocking(); + int result = av_read_frame(format_context_, packet); + EndBlocking(); + + return result == AVERROR_EOF ? result : TranslateError(result); } int MediaReader::Seek(int64_t position_us) @@ -201,7 +256,55 @@ namespace ffmpeg // AVSEEK_FLAG_BACKWARD lands on the keyframe at or before the target, // so that what follows can actually be decoded. Frames between that // keyframe and the target are decoded and dropped by the caller. - return av_seek_frame(format_context_, -1, position_us, AVSEEK_FLAG_BACKWARD); + BeginBlocking(); + int result = av_seek_frame(format_context_, -1, position_us, AVSEEK_FLAG_BACKWARD); + EndBlocking(); + + return TranslateError(result); + } + + void MediaReader::Interrupt() + { + interrupted_.store(true); + } + + int MediaReader::OnInterrupt(void * opaque) + { + MediaReader * reader = static_cast(opaque); + + if (reader->interrupted_.load()) { + return 1; + } + + const int64_t deadline = reader->deadline_us_.load(); + + if (deadline != 0 && av_gettime_relative() > deadline) { + reader->timed_out_.store(true); + + return 1; + } + + return 0; + } + + void MediaReader::BeginBlocking() + { + timed_out_.store(false); + deadline_us_.store(timeout_us_ > 0 ? av_gettime_relative() + timeout_us_ : 0); + } + + void MediaReader::EndBlocking() + { + deadline_us_.store(0); + } + + int MediaReader::TranslateError(int error) const + { + if (error < 0 && !interrupted_.load() && timed_out_.load()) { + return AVERROR(ETIMEDOUT); + } + + return error; } const AVStream * MediaReader::GetVideoStream() const diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java index 3573f236..83869bb5 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.time.Duration; import dev.onvoid.webrtc.media.audio.CustomAudioSource; import dev.onvoid.webrtc.media.video.CustomVideoSource; @@ -82,12 +83,27 @@ public MediaFileSource(Path path) throws IOException { /** * Opens the given media source. * - * @param source The path or URL of the source to play. + * @param source The path or {@code rtsp://} URL of the source to play. * * @throws IOException if the source cannot be opened or decoded. */ public MediaFileSource(String source) throws IOException { - MediaReader reader = new MediaReader(source); + this(source, MediaReader.DEFAULT_TIMEOUT); + } + + /** + * Opens the given media source, allowing each operation on it the given + * time before it fails, as {@link MediaReader} describes. + * + * @param source The path or {@code rtsp://} URL of the source to play. + * @param timeout How long opening, and later any single read, may wait on + * the source. {@link Duration#ZERO} waits as long as it + * takes. + * + * @throws IOException if the source cannot be opened in time, or decoded. + */ + public MediaFileSource(String source, Duration timeout) throws IOException { + MediaReader reader = new MediaReader(source, timeout); // Read while the reader is still ours: the player takes it over. info = reader.getInfo(); diff --git a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaReader.java b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaReader.java index 4021ff7c..d420eb4a 100644 --- a/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaReader.java +++ b/webrtc-java-media/src/main/java/dev/onvoid/webrtc/media/player/MediaReader.java @@ -18,14 +18,21 @@ import java.io.IOException; import java.nio.file.Path; +import java.time.Duration; +import java.util.Objects; /** * An opened media source, and what it contains. *

      - * A source is anything FFmpeg can demux: a media file today, and an http, rtsp - * or rtmp URL once those protocols are enabled in the build. Opening reads the - * container and picks the video and audio stream that are meant to be played; - * a source with neither fails to open. + * A source is a media file, or a live stream behind an {@code rtsp://} URL, + * such as an IP camera. Opening reads the container and picks the video and + * audio stream that are meant to be played; a source with neither fails to + * open. + *

      + * Any single operation that waits on the source gives up once the reader's + * timeout has passed: opening it, reading from it during playback, seeking in + * it and closing it. For a file that is never an issue; for a stream it is + * what keeps an unreachable or stalled server from blocking indefinitely. *

      * A reader holds a native resource and has to be closed. It is not safe to use * from several threads at once, other than {@link #close()}, which may be @@ -39,6 +46,11 @@ public class MediaReader implements AutoCloseable { FFmpeg.load(); } + /** + * How long an operation on the source may wait when no timeout is given. + */ + public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10); + /** The native reader, or 0 once this reader has been closed. */ private long handle; @@ -58,13 +70,37 @@ public MediaReader(Path path) throws IOException { /** * Opens the given media source. * - * @param source The path or URL of the source to open. + * @param source The path or {@code rtsp://} URL of the source to open. * * @throws IOException if the source cannot be opened, or holds nothing * that can be played. */ public MediaReader(String source) throws IOException { - handle = open(source); + this(source, DEFAULT_TIMEOUT); + } + + /** + * Opens the given media source, allowing each operation on it the given + * time before it fails. + * + * @param source The path or {@code rtsp://} URL of the source to open. + * @param timeout How long opening, and later any single read, seek or + * close, may wait on the source. {@link Duration#ZERO} + * waits as long as it takes. + * + * @throws IOException if the source cannot be opened in + * time, or holds nothing that can be + * played. + * @throws IllegalArgumentException if the timeout is negative. + */ + public MediaReader(String source, Duration timeout) throws IOException { + Objects.requireNonNull(timeout, "Timeout is null"); + + if (timeout.isNegative()) { + throw new IllegalArgumentException("Timeout is negative: " + timeout); + } + + handle = open(source, toMicros(timeout)); } /** @@ -111,7 +147,17 @@ synchronized long detach() { return detaching; } - private static native long open(String source) throws IOException; + private static long toMicros(Duration duration) { + try { + return duration.toNanos() / 1000; + } + catch (ArithmeticException e) { + // Longer than nanoseconds can count, which is as good as never. + return 0; + } + } + + private static native long open(String source, long timeoutUs) throws IOException; private static native MediaInfo info(long handle); diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/RtspStreamTest.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/RtspStreamTest.java new file mode 100644 index 00000000..1b44d2fc --- /dev/null +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/RtspStreamTest.java @@ -0,0 +1,275 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc.media.player; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import dev.onvoid.webrtc.PeerConnectionFactory; +import dev.onvoid.webrtc.media.audio.AudioDeviceModule; +import dev.onvoid.webrtc.media.audio.AudioLayer; +import dev.onvoid.webrtc.media.audio.AudioTrack; +import dev.onvoid.webrtc.media.audio.AudioTrackSink; +import dev.onvoid.webrtc.media.audio.CustomAudioSource; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Tests playing live streams over RTSP, from a server in this process, and + * what keeps a stream that misbehaves from blocking: the timeouts, and closing + * a player that is waiting on one. + * + * @author Alex Andres + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Execution(ExecutionMode.SAME_THREAD) +class RtspStreamTest { + + private AudioDeviceModule audioModule; + private PeerConnectionFactory factory; + + + @BeforeAll + void initFactory() { + audioModule = new AudioDeviceModule(AudioLayer.kDummyAudio); + factory = new PeerConnectionFactory(audioModule); + } + + @AfterAll + void disposeFactory() { + factory.dispose(); + audioModule.dispose(); + } + + @Test + void describesStream() throws Exception { + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.END, 100); + MediaReader reader = new MediaReader(server.url())) { + MediaInfo info = reader.getInfo(); + + assertFalse(info.hasVideo()); + assertTrue(info.hasAudio()); + assertEquals(TestRtspServer.SAMPLE_RATE, info.getSampleRate()); + assertEquals(1, info.getChannels()); + } + } + + @Test + void playsStream() throws Exception { + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.END, 100); + Playback playback = new Playback(new MediaReader(server.url()))) { + playback.player.play(); + + // One second of audio, sent in real time, and the end of the + // stream once the server hangs up. + assertTrue(playback.done.await(10, TimeUnit.SECONDS), "stream did not end"); + assertTrue(playback.chunks.get() >= 90, "audio chunks: " + playback.chunks.get()); + assertTrue(playback.nonSilent.get() > 0, "only silence arrived"); + } + } + + @Test + void liveStreamEndsDespiteLooping() throws Exception { + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.END, 50); + Playback playback = new Playback(new MediaReader(server.url()))) { + // A live stream cannot start over, so looping it has to end like + // any stream, rather than try to rewind forever. + playback.player.setLooping(true); + playback.player.play(); + + assertTrue(playback.done.await(10, TimeUnit.SECONDS), "stream did not end"); + assertEquals(MediaPlayerState.ENDED, playback.player.getState(), + "error: " + playback.error.get()); + } + } + + @Test + void openTimesOut() throws Exception { + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.SILENT, 0)) { + long start = System.nanoTime(); + + IOException e = assertThrows(IOException.class, + () -> new MediaReader(server.url(), Duration.ofMillis(500))); + + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + assertTrue(elapsedMs < 5000, "took " + elapsedMs + " ms"); + assertTrue(e.getMessage().toLowerCase().contains("timed out"), e.getMessage()); + } + } + + @Test + void errorHidesCredentials() throws Exception { + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.SILENT, 0)) { + String url = server.url().replace("rtsp://", "rtsp://admin:s3cret@"); + + IOException e = assertThrows(IOException.class, + () -> new MediaReader(url, Duration.ofMillis(300))); + + // Messages end up in logs; the password must not. + assertFalse(e.getMessage().contains("s3cret"), e.getMessage()); + assertFalse(e.getMessage().contains("admin"), e.getMessage()); + assertTrue(e.getMessage().contains("127.0.0.1:" + server.port() + "/stream"), + e.getMessage()); + } + } + + @Test + void stalledStreamTimesOut() throws Exception { + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.STALL, 30); + Playback playback = new Playback(new MediaReader(server.url(), Duration.ofSeconds(1)))) { + playback.player.play(); + + assertTrue(playback.done.await(10, TimeUnit.SECONDS), "stall not reported"); + + String error = playback.error.get(); + + assertTrue(error != null && error.toLowerCase().contains("timed out"), + "error: " + error); + assertEquals(MediaPlayerState.PAUSED, playback.player.getState()); + + // Reported, so not an error of the playback itself. + playback.error.set(null); + } + } + + @Test + void closeInterruptsStalledStream() throws Exception { + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.STALL, 30); + Playback playback = new Playback(new MediaReader(server.url(), Duration.ofMinutes(5)))) { + playback.player.play(); + + // Let it run into the stall, where the player's thread waits on a + // read that would otherwise take five minutes to give up. + assertTrue(waitFor(() -> playback.chunks.get() >= 25, 10), "stream did not start"); + Thread.sleep(500); + + long start = System.nanoTime(); + + playback.player.close(); + + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + assertTrue(elapsedMs < 3000, "close took " + elapsedMs + " ms"); + } + } + + @Test + void refusesOtherProtocols() throws Exception { + // The build carries HTTP for RTSP tunnelled over it, but a source must + // not be able to use it on its own. + try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.SILENT, 0)) { + assertThrows(IOException.class, () -> + new MediaReader("http://127.0.0.1:" + server.port() + "/file.mp4", + Duration.ofSeconds(2))); + + assertEquals(0, server.connections(), "the server was contacted"); + } + } + + private static boolean waitFor(java.util.function.BooleanSupplier condition, + int seconds) throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds); + + while (!condition.getAsBoolean()) { + if (System.nanoTime() > deadline) { + return false; + } + + Thread.sleep(20); + } + + return true; + } + + /** + * An audio source fed by a player, and the track sink that counts what + * arrives. + */ + private final class Playback implements AutoCloseable { + + final CustomAudioSource audioSource = new CustomAudioSource(); + final AudioTrack audioTrack = factory.createAudioTrack("audio", audioSource); + final MediaPlayer player; + + final CountDownLatch done = new CountDownLatch(1); + final AtomicInteger chunks = new AtomicInteger(); + final AtomicInteger nonSilent = new AtomicInteger(); + final AtomicReference error = new AtomicReference<>(); + + private final AudioTrackSink sink = (data, bits, rate, channels, frames) -> { + chunks.incrementAndGet(); + + for (byte b : data) { + if (b != 0) { + nonSilent.incrementAndGet(); + break; + } + } + }; + + + Playback(MediaReader reader) throws IOException { + audioTrack.addSink(sink); + + player = new MediaPlayer(reader, null, audioSource); + player.setListener(new MediaPlayerListener() { + + @Override + public void onEndOfStream() { + done.countDown(); + } + + @Override + public void onError(String message) { + error.set(message); + done.countDown(); + } + }); + } + + @Override + public void close() { + player.close(); + + audioTrack.removeSink(sink); + audioTrack.dispose(); + audioSource.dispose(); + + String message = error.get(); + + if (message != null) { + throw new AssertionError("playback reported an error: " + message); + } + } + } + +} diff --git a/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestRtspServer.java b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestRtspServer.java new file mode 100644 index 00000000..3f9a2119 --- /dev/null +++ b/webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/TestRtspServer.java @@ -0,0 +1,350 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc.media.player; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Just enough of an RTSP server to play a live stream to the media module: it + * answers OPTIONS, DESCRIBE, SETUP and PLAY, and then streams 48 kHz mono + * 16-bit PCM (L16) in 10 ms RTP packets, interleaved on the RTSP connection. + *

      + * It refuses RTP over UDP with 461, as a server behind a firewall often has + * to, which is what makes the client fall back to TCP, the way most real + * deployments end up. + * + * @author Alex Andres + */ +final class TestRtspServer implements AutoCloseable { + + /** What the server does once it has sent its packets, or instead. */ + enum Behavior { + + /** Closes the connection, as a stream that ends. */ + END, + + /** Keeps the connection open and sends nothing more. */ + STALL, + + /** Accepts connections and never answers anything on them. */ + SILENT + } + + static final int SAMPLE_RATE = 48000; + + /** 10 ms of samples per packet. */ + private static final int SAMPLES_PER_PACKET = SAMPLE_RATE / 100; + + private static final int PAYLOAD_TYPE = 96; + + private final ServerSocket server; + + private final Behavior behavior; + + private final int packets; + + private final List sockets = new CopyOnWriteArrayList<>(); + + private final AtomicInteger connections = new AtomicInteger(); + + private final CountDownLatch closed = new CountDownLatch(1); + + + /** + * Starts serving on a free port of the loopback address. + * + * @param behavior What to do once the packets are sent. + * @param packets How many 10 ms packets to stream after PLAY. + */ + TestRtspServer(Behavior behavior, int packets) throws IOException { + this.behavior = behavior; + this.packets = packets; + this.server = new ServerSocket(0, 50, InetAddress.getLoopbackAddress()); + + Thread acceptor = new Thread(this::accept, "test-rtsp-accept"); + acceptor.setDaemon(true); + acceptor.start(); + } + + /** + * @return The URL of the stream. + */ + String url() { + return "rtsp://127.0.0.1:" + server.getLocalPort() + "/stream"; + } + + /** + * @return The port the server listens on. + */ + int port() { + return server.getLocalPort(); + } + + /** + * @return How many connections the server has accepted. + */ + int connections() { + return connections.get(); + } + + @Override + public void close() throws IOException { + closed.countDown(); + server.close(); + + for (Socket socket : sockets) { + socket.close(); + } + } + + private void accept() { + try { + while (true) { + Socket socket = server.accept(); + + connections.incrementAndGet(); + sockets.add(socket); + + Thread handler = new Thread(() -> handle(socket), "test-rtsp-connection"); + handler.setDaemon(true); + handler.start(); + } + } + catch (IOException e) { + // Closed. + } + } + + private void handle(Socket socket) { + try { + if (behavior == Behavior.SILENT) { + awaitClose(); + return; + } + + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + + while (true) { + String requestLine = readLine(in); + + if (requestLine == null) { + return; + } + if (requestLine.isEmpty()) { + continue; + } + + String method = requestLine.split(" ")[0]; + String cseq = "0"; + String transport = ""; + int contentLength = 0; + + for (String header = readLine(in); header != null && !header.isEmpty(); + header = readLine(in)) { + String name = header.substring(0, Math.max(0, header.indexOf(':'))) + .trim().toLowerCase(Locale.ROOT); + String value = header.substring(header.indexOf(':') + 1).trim(); + + if (name.equals("cseq")) { + cseq = value; + } + else if (name.equals("transport")) { + transport = value; + } + else if (name.equals("content-length")) { + contentLength = Integer.parseInt(value); + } + } + + skip(in, contentLength); + + switch (method) { + case "OPTIONS": + respond(out, 200, "OK", cseq, + "Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN\r\n", ""); + break; + + case "DESCRIBE": + String sdp = "v=0\r\n" + + "o=- 0 0 IN IP4 127.0.0.1\r\n" + + "s=Test\r\n" + + "c=IN IP4 127.0.0.1\r\n" + + "t=0 0\r\n" + + "m=audio 0 RTP/AVP " + PAYLOAD_TYPE + "\r\n" + + "a=rtpmap:" + PAYLOAD_TYPE + " L16/" + SAMPLE_RATE + "/1\r\n" + + "a=control:track1\r\n"; + + respond(out, 200, "OK", cseq, + "Content-Base: " + url() + "/\r\n" + + "Content-Type: application/sdp\r\n", sdp); + break; + + case "SETUP": + if (!transport.contains("TCP")) { + respond(out, 461, "Unsupported Transport", cseq, "", ""); + } + else { + respond(out, 200, "OK", cseq, + "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n" + + "Session: 12345678\r\n", ""); + } + break; + + case "PLAY": + respond(out, 200, "OK", cseq, "Session: 12345678\r\n", ""); + + stream(out); + + if (behavior == Behavior.END) { + socket.close(); + } + else { + awaitClose(); + } + return; + + default: + // TEARDOWN, keep-alives and whatever else. + respond(out, 200, "OK", cseq, "Session: 12345678\r\n", ""); + break; + } + } + } + catch (IOException e) { + // The client went away, or the server was closed. + } + } + + /** + * Sends the packets in real time, as a live source would: a sawtooth that + * is never zero, so that what arrives can be told apart from silence. + */ + private void stream(OutputStream out) throws IOException { + long start = System.nanoTime(); + int sample = 0; + + for (int i = 0; i < packets; i++) { + ByteArrayOutputStream packet = new ByteArrayOutputStream(); + + // RTP version 2, no padding, extension or CSRCs; no marker. + packet.write(0x80); + packet.write(PAYLOAD_TYPE); + writeInt(packet, i, 2); + writeInt(packet, (long) i * SAMPLES_PER_PACKET, 4); + writeInt(packet, 0x12345678L, 4); + + for (int n = 0; n < SAMPLES_PER_PACKET; n++, sample++) { + // L16 is big-endian. + writeInt(packet, (1000 + (sample * 37) % 16000) & 0xFFFF, 2); + } + + byte[] rtp = packet.toByteArray(); + + // Interleaved on channel 0. + out.write('$'); + out.write(0); + out.write((rtp.length >> 8) & 0xFF); + out.write(rtp.length & 0xFF); + out.write(rtp); + out.flush(); + + long due = start + TimeUnit.MILLISECONDS.toNanos(10L * (i + 1)); + long wait = due - System.nanoTime(); + + if (wait > 0) { + try { + TimeUnit.NANOSECONDS.sleep(wait); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + } + + private void awaitClose() { + try { + closed.await(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void respond(OutputStream out, int status, String reason, String cseq, + String headers, String body) throws IOException { + byte[] content = body.getBytes(StandardCharsets.US_ASCII); + String response = "RTSP/1.0 " + status + " " + reason + "\r\n" + + "CSeq: " + cseq + "\r\n" + + headers + + (content.length > 0 ? "Content-Length: " + content.length + "\r\n" : "") + + "\r\n"; + + out.write(response.getBytes(StandardCharsets.US_ASCII)); + out.write(content); + out.flush(); + } + + private static String readLine(InputStream in) throws IOException { + StringBuilder line = new StringBuilder(); + + for (int c = in.read(); c != -1; c = in.read()) { + if (c == '\n') { + int end = line.length(); + + if (end > 0 && line.charAt(end - 1) == '\r') { + line.setLength(end - 1); + } + return line.toString(); + } + + line.append((char) c); + } + + return line.length() > 0 ? line.toString() : null; + } + + private static void skip(InputStream in, int count) throws IOException { + for (int i = 0; i < count; i++) { + if (in.read() == -1) { + return; + } + } + } + + private static void writeInt(ByteArrayOutputStream out, long value, int bytes) { + for (int shift = (bytes - 1) * 8; shift >= 0; shift -= 8) { + out.write((int) (value >>> shift) & 0xFF); + } + } + +}