From 5302cac8cf086a00b7dc8bdc211f0172895d44df Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 10 Oct 2026 14:33:13 +0200 Subject: [PATCH] feat: let the audio codecs of a factory be limited and ordered PeerConnectionFactory.Builder gains setAudioEncoderFactory and setAudioDecoderFactory, which take the new BuiltinAudioEncoderFactory and BuiltinAudioDecoderFactory of dev.onvoid.webrtc.media.audio.codec. Without codec names they are WebRTC's built-in factories, as before. With names they offer only those codecs, in that order, for example only Opus; an unknown name throws and lists the built-in codecs. getSupportedCodecs() returns the resulting AudioCodecInfo list. Natively, FilteredAudioEncoderFactory and FilteredAudioDecoderFactory wrap the built-in factories, selecting and ordering by name the same way the Java side does, and refusing to create codecs that were not selected. WebRTC still adds telephone-event, CN and red by itself. The factory classes are abstract with package-private constructors, so that Java audio codecs can be added later without changing the builder. --- docs/.vitepress/sidebar.ts | 1 + docs/guide/advanced/audio-codecs.md | 50 +++++ docs/guide/index.md | 1 + webrtc-jni/src/main/cpp/CMakeLists.txt | 2 + .../include/JNI_BuiltinAudioDecoderFactory.h | 21 ++ .../include/JNI_BuiltinAudioEncoderFactory.h | 21 ++ .../cpp/include/JNI_PeerConnectionFactory.h | 4 +- webrtc-jni/src/main/cpp/include/JNI_WebRTC.h | 1 + .../media/audio/codec/AudioCodecFactories.h | 41 ++++ .../media/audio/codec/AudioCodecInfo.h | 48 +++++ .../media/audio/codec/AudioCodecSelection.h | 38 ++++ .../audio/codec/FilteredAudioDecoderFactory.h | 53 +++++ .../audio/codec/FilteredAudioEncoderFactory.h | 51 +++++ .../src/JNI_BuiltinAudioDecoderFactory.cpp | 36 ++++ .../src/JNI_BuiltinAudioEncoderFactory.cpp | 36 ++++ .../cpp/src/JNI_PeerConnectionFactory.cpp | 13 +- .../media/audio/codec/AudioCodecFactories.cpp | 92 ++++++++ .../src/media/audio/codec/AudioCodecInfo.cpp | 74 +++++++ .../media/audio/codec/AudioCodecSelection.cpp | 49 +++++ .../codec/FilteredAudioDecoderFactory.cpp | 50 +++++ .../codec/FilteredAudioEncoderFactory.cpp | 54 +++++ .../onvoid/webrtc/PeerConnectionFactory.java | 60 +++++- .../media/audio/codec/AudioCodecInfo.java | 114 ++++++++++ .../audio/codec/AudioCodecSelection.java | 109 ++++++++++ .../audio/codec/AudioDecoderFactory.java | 43 ++++ .../audio/codec/AudioEncoderFactory.java | 43 ++++ .../codec/BuiltinAudioDecoderFactory.java | 73 +++++++ .../codec/BuiltinAudioEncoderFactory.java | 83 +++++++ webrtc/src/main/java/module-info.java | 1 + .../onvoid/webrtc/AudioCodecFactoryTests.java | 202 ++++++++++++++++++ 30 files changed, 1449 insertions(+), 15 deletions(-) create mode 100644 docs/guide/advanced/audio-codecs.md create mode 100644 webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioDecoderFactory.h create mode 100644 webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioEncoderFactory.h create mode 100644 webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecFactories.h create mode 100644 webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecInfo.h create mode 100644 webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecSelection.h create mode 100644 webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioDecoderFactory.h create mode 100644 webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioEncoderFactory.h create mode 100644 webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioDecoderFactory.cpp create mode 100644 webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioEncoderFactory.cpp create mode 100644 webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecFactories.cpp create mode 100644 webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecInfo.cpp create mode 100644 webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecSelection.cpp create mode 100644 webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioDecoderFactory.cpp create mode 100644 webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioEncoderFactory.cpp create mode 100644 webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecInfo.java create mode 100644 webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecSelection.java create mode 100644 webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioDecoderFactory.java create mode 100644 webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioEncoderFactory.java create mode 100644 webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioDecoderFactory.java create mode 100644 webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioEncoderFactory.java create mode 100644 webrtc/src/test/java/dev/onvoid/webrtc/AudioCodecFactoryTests.java diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index be79fb5d9..a4d3ab087 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -76,6 +76,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { { text: 'Field Trials', link: '/advanced/field-trials' }, { text: 'Encoded Transforms', link: '/advanced/encoded-transforms' }, { text: 'Video Codecs', link: '/advanced/video-codecs' }, + { text: 'Audio Codecs', link: '/advanced/audio-codecs' }, ], }, ] diff --git a/docs/guide/advanced/audio-codecs.md b/docs/guide/advanced/audio-codecs.md new file mode 100644 index 000000000..23aaf1837 --- /dev/null +++ b/docs/guide/advanced/audio-codecs.md @@ -0,0 +1,50 @@ +# Audio Codecs + +A `PeerConnectionFactory` decides which audio codecs it can send and receive through an audio encoder factory and an audio decoder factory. Unless told otherwise it uses all codecs built into WebRTC. This guide explains how to limit them, for example to offer only Opus, or to put a codec first that a remote endpoint expects. + +The API lives in the `dev.onvoid.webrtc.media.audio.codec` package. + +## Built-in Codecs + +`BuiltinAudioEncoderFactory` and `BuiltinAudioDecoderFactory` provide WebRTC's audio codecs: Opus, G722, PCMU and PCMA. Ask a factory for the exact list: + +```java +for (AudioCodecInfo codec : new BuiltinAudioEncoderFactory().getSupportedCodecs()) { + System.out.println(codec.getName() + "/" + codec.getClockRate() + "/" + codec.getChannels() + + " " + codec.getParameters()); +} +``` + +## Limiting and Ordering the Codecs + +Pass codec names to a factory to keep only those codecs, in the given order of preference. Names are compared ignoring case, and a name that is not a built-in codec throws an `IllegalArgumentException` that lists the valid ones: + +```java +PeerConnectionFactory factory = PeerConnectionFactory.builder() + .setAudioEncoderFactory(new BuiltinAudioEncoderFactory("opus")) + .setAudioDecoderFactory(new BuiltinAudioDecoderFactory("opus", "PCMU")) + .build(); +``` + +The encoder factory decides what the factory can send and in which order its offers list the codecs; the decoder factory decides what it can receive. Without names, or without a factory set, all built-in codecs are used. + +WebRTC adds the auxiliary codecs by itself, depending on the selected ones: + +| Codec | Added when | +|---|---| +| `telephone-event` | A selected codec has a clock rate of 8000 or 48000 Hz (Opus, G722, PCMU, PCMA all do), so DTMF keeps working | +| `CN` | A selected codec runs at 8000 Hz and allows comfort noise | +| `red` | Opus is selected | + +`getSupportedCodecs()` does not list them; `PeerConnectionFactory.getRtpSenderCapabilities(MediaType.AUDIO)` does. + +::: tip +To change the codec order of a single connection rather than of the whole factory, use `RTCRtpTransceiver.setCodecPreferences()`. A factory's codec list is the set those preferences choose from. +::: + +## Related API + +- `PeerConnectionFactory.Builder.setAudioEncoderFactory()`, `setAudioDecoderFactory()` — set the factories. +- `BuiltinAudioEncoderFactory`, `BuiltinAudioDecoderFactory` — WebRTC's codecs, optionally limited. +- `AudioCodecInfo` — a codec's name, clock rate, channels and format parameters. +- [Video Codecs](/guide/advanced/video-codecs) — the same for video, including codecs written in Java. diff --git a/docs/guide/index.md b/docs/guide/index.md index daffae65f..0b6912e80 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -43,6 +43,7 @@ This section provides detailed guides for various features of the webrtc-java li - [Field Trials](/guide/advanced/field-trials) - Enabling experimental features and tuning WebRTC internals - [Encoded Transforms](/guide/advanced/encoded-transforms) - Reading and changing encoded frames, e.g. for end-to-end encryption - [Video Codecs](/guide/advanced/video-codecs) - Adding video codecs implemented in Java, or limiting the built-in ones +- [Audio Codecs](/guide/advanced/audio-codecs) - Limiting and ordering the built-in audio codecs ## Additional Resources diff --git a/webrtc-jni/src/main/cpp/CMakeLists.txt b/webrtc-jni/src/main/cpp/CMakeLists.txt index c4134cdcd..0b22045c8 100644 --- a/webrtc-jni/src/main/cpp/CMakeLists.txt +++ b/webrtc-jni/src/main/cpp/CMakeLists.txt @@ -54,6 +54,7 @@ file(GLOB SOURCES_API_AUDIO "src/api/audio/*.cpp") file(GLOB SOURCES_MEDIA "src/media/*.cpp") file(GLOB SOURCES_MEDIA_AUDIO "src/media/audio/*.cpp") file(GLOB SOURCES_MEDIA_AUDIO_OS "src/media/audio/${SOURCE_TARGET}/*.cpp") +file(GLOB SOURCES_MEDIA_AUDIO_CODEC "src/media/audio/codec/*.cpp") file(GLOB SOURCES_MEDIA_VIDEO "src/media/video/*.cpp") file(GLOB SOURCES_MEDIA_VIDEO_CODEC "src/media/video/codec/*.cpp") file(GLOB SOURCES_MEDIA_VIDEO_CODEC_OS "src/media/video/codec/${SOURCE_TARGET}/*.cpp") @@ -81,6 +82,7 @@ list(APPEND SOURCES ${SOURCES_MEDIA} ${SOURCES_MEDIA_AUDIO} ${SOURCES_MEDIA_AUDIO_OS} + ${SOURCES_MEDIA_AUDIO_CODEC} ${SOURCES_MEDIA_VIDEO} ${SOURCES_MEDIA_VIDEO_CODEC} ${SOURCES_MEDIA_VIDEO_CODEC_OS} diff --git a/webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioDecoderFactory.h b/webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioDecoderFactory.h new file mode 100644 index 000000000..f5a3da9f7 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioDecoderFactory.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class dev_onvoid_webrtc_media_audio_codec_BuiltinAudioDecoderFactory */ + +#ifndef _Included_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioDecoderFactory +#define _Included_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioDecoderFactory +#ifdef __cplusplus +extern "C" { +#endif + /* + * Class: dev_onvoid_webrtc_media_audio_codec_BuiltinAudioDecoderFactory + * Method: getBuiltinCodecs + * Signature: ()[Ldev/onvoid/webrtc/media/audio/codec/AudioCodecInfo; + */ + JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioDecoderFactory_getBuiltinCodecs + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioEncoderFactory.h b/webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioEncoderFactory.h new file mode 100644 index 000000000..49c2d59c3 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/JNI_BuiltinAudioEncoderFactory.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class dev_onvoid_webrtc_media_audio_codec_BuiltinAudioEncoderFactory */ + +#ifndef _Included_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioEncoderFactory +#define _Included_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioEncoderFactory +#ifdef __cplusplus +extern "C" { +#endif + /* + * Class: dev_onvoid_webrtc_media_audio_codec_BuiltinAudioEncoderFactory + * Method: getBuiltinCodecs + * Signature: ()[Ldev/onvoid/webrtc/media/audio/codec/AudioCodecInfo; + */ + JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioEncoderFactory_getBuiltinCodecs + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h b/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h index c5b40c021..0edbfb961 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h +++ b/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h @@ -82,10 +82,10 @@ extern "C" { /* * Class: dev_onvoid_webrtc_PeerConnectionFactory * Method: initialize - * Signature: (Ljava/util/Map;Ldev/onvoid/webrtc/media/audio/AudioDeviceModuleBase;Ldev/onvoid/webrtc/media/audio/AudioProcessing;Ldev/onvoid/webrtc/media/video/codec/VideoEncoderFactory;Ldev/onvoid/webrtc/media/video/codec/VideoDecoderFactory;)V + * Signature: (Ljava/util/Map;Ldev/onvoid/webrtc/media/audio/AudioDeviceModuleBase;Ldev/onvoid/webrtc/media/audio/AudioProcessing;Ldev/onvoid/webrtc/media/video/codec/VideoEncoderFactory;Ldev/onvoid/webrtc/media/video/codec/VideoDecoderFactory;Ldev/onvoid/webrtc/media/audio/codec/AudioEncoderFactory;Ldev/onvoid/webrtc/media/audio/codec/AudioDecoderFactory;)V */ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_initialize - (JNIEnv *, jobject, jobject, jobject, jobject, jobject, jobject); + (JNIEnv *, jobject, jobject, jobject, jobject, jobject, jobject, jobject, jobject); #ifdef __cplusplus } diff --git a/webrtc-jni/src/main/cpp/include/JNI_WebRTC.h b/webrtc-jni/src/main/cpp/include/JNI_WebRTC.h index 6d5d6b568..a568cab73 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_WebRTC.h +++ b/webrtc-jni/src/main/cpp/include/JNI_WebRTC.h @@ -22,6 +22,7 @@ #define PKG_LOG "dev/onvoid/webrtc/logging/" #define PKG_MEDIA "dev/onvoid/webrtc/media/" #define PKG_AUDIO "dev/onvoid/webrtc/media/audio/" +#define PKG_AUDIO_CODEC "dev/onvoid/webrtc/media/audio/codec/" #define PKG_VIDEO "dev/onvoid/webrtc/media/video/" #define PKG_CODEC "dev/onvoid/webrtc/media/video/codec/" #define PKG_DESKTOP "dev/onvoid/webrtc/media/video/desktop/" diff --git a/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecFactories.h b/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecFactories.h new file mode 100644 index 000000000..25ab7a5e0 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecFactories.h @@ -0,0 +1,41 @@ +/* + * 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 JNI_WEBRTC_MEDIA_AUDIO_CODEC_AUDIO_CODEC_FACTORIES_H_ +#define JNI_WEBRTC_MEDIA_AUDIO_CODEC_AUDIO_CODEC_FACTORIES_H_ + +#include "JavaRef.h" + +#include "api/audio_codecs/audio_decoder_factory.h" +#include "api/audio_codecs/audio_encoder_factory.h" +#include "api/scoped_refptr.h" + +#include + +namespace jni +{ + // Creates the native factory for a Java BuiltinAudioEncoderFactory: the + // built-in factory, limited to the codecs the Java object selected. A + // null object stands for all built-in codecs. + webrtc::scoped_refptr CreateAudioEncoderFactory(JNIEnv * env, + const JavaRef & factory); + + // The same for a Java BuiltinAudioDecoderFactory. + webrtc::scoped_refptr CreateAudioDecoderFactory(JNIEnv * env, + const JavaRef & factory); +} + +#endif diff --git a/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecInfo.h b/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecInfo.h new file mode 100644 index 000000000..55ec241c1 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecInfo.h @@ -0,0 +1,48 @@ +/* + * 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 JNI_WEBRTC_MEDIA_AUDIO_CODEC_AUDIO_CODEC_INFO_H_ +#define JNI_WEBRTC_MEDIA_AUDIO_CODEC_AUDIO_CODEC_INFO_H_ + +#include "JavaClass.h" +#include "JavaRef.h" + +#include "api/audio_codecs/audio_format.h" + +#include + +#include + +namespace jni +{ + namespace AudioCodecInfo + { + JavaLocalRef toJava(JNIEnv * env, const webrtc::SdpAudioFormat & format); + + JavaLocalRef toJavaArray(JNIEnv * env, const std::vector & specs); + + class JavaAudioCodecInfoClass : public JavaClass + { + public: + explicit JavaAudioCodecInfoClass(JNIEnv * env); + + jclass cls; + jmethodID ctor; + }; + } +} + +#endif diff --git a/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecSelection.h b/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecSelection.h new file mode 100644 index 000000000..5bbc54ea3 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/media/audio/codec/AudioCodecSelection.h @@ -0,0 +1,38 @@ +/* + * 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 JNI_WEBRTC_MEDIA_AUDIO_CODEC_AUDIO_CODEC_SELECTION_H_ +#define JNI_WEBRTC_MEDIA_AUDIO_CODEC_AUDIO_CODEC_SELECTION_H_ + +#include "api/audio_codecs/audio_format.h" + +#include +#include + +namespace jni +{ + // Whether a codec name is one of the selected names, ignoring case as SDP + // does. + bool IsSelectedAudioCodec(const std::string & name, const std::vector & names); + + // Returns the codecs with the selected names, ordered by the names and, + // for one name, by their order in `specs`. The Java side, + // AudioCodecSelection.select, orders the same way. + std::vector SelectAudioCodecs(const std::vector & specs, + const std::vector & names); +} + +#endif diff --git a/webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioDecoderFactory.h b/webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioDecoderFactory.h new file mode 100644 index 000000000..9385bd9e7 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioDecoderFactory.h @@ -0,0 +1,53 @@ +/* + * 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 JNI_WEBRTC_MEDIA_AUDIO_CODEC_FILTERED_AUDIO_DECODER_FACTORY_H_ +#define JNI_WEBRTC_MEDIA_AUDIO_CODEC_FILTERED_AUDIO_DECODER_FACTORY_H_ + +#include "api/audio_codecs/audio_decoder_factory.h" +#include "api/scoped_refptr.h" + +#include +#include +#include + +namespace jni +{ + // Offers and creates only the decoders of another factory that have one + // of the given names, in the order of the names. + class FilteredAudioDecoderFactory : public webrtc::AudioDecoderFactory + { + public: + FilteredAudioDecoderFactory(webrtc::scoped_refptr factory, + std::vector names); + + std::vector GetSupportedDecoders() override; + + bool IsSupportedDecoder(const webrtc::SdpAudioFormat & format) override; + + // The variant with a codec pair ID calls this one. + using webrtc::AudioDecoderFactory::Create; + + std::unique_ptr Create(const webrtc::Environment & env, + const webrtc::SdpAudioFormat & format) override; + + private: + const webrtc::scoped_refptr factory; + const std::vector names; + }; +} + +#endif diff --git a/webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioEncoderFactory.h b/webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioEncoderFactory.h new file mode 100644 index 000000000..782b9f6b0 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/media/audio/codec/FilteredAudioEncoderFactory.h @@ -0,0 +1,51 @@ +/* + * 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 JNI_WEBRTC_MEDIA_AUDIO_CODEC_FILTERED_AUDIO_ENCODER_FACTORY_H_ +#define JNI_WEBRTC_MEDIA_AUDIO_CODEC_FILTERED_AUDIO_ENCODER_FACTORY_H_ + +#include "api/audio_codecs/audio_encoder_factory.h" +#include "api/scoped_refptr.h" + +#include +#include +#include +#include + +namespace jni +{ + // Offers and creates only the codecs of another factory that have one of + // the given names, in the order of the names. + class FilteredAudioEncoderFactory : public webrtc::AudioEncoderFactory + { + public: + FilteredAudioEncoderFactory(webrtc::scoped_refptr factory, + std::vector names); + + std::vector GetSupportedEncoders() override; + + std::optional QueryAudioEncoder(const webrtc::SdpAudioFormat & format) override; + + std::unique_ptr Create(const webrtc::Environment & env, + const webrtc::SdpAudioFormat & format, Options options) override; + + private: + const webrtc::scoped_refptr factory; + const std::vector names; + }; +} + +#endif diff --git a/webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioDecoderFactory.cpp b/webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioDecoderFactory.cpp new file mode 100644 index 000000000..7fe925634 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioDecoderFactory.cpp @@ -0,0 +1,36 @@ +/* + * 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 "JNI_BuiltinAudioDecoderFactory.h" +#include "media/audio/codec/AudioCodecInfo.h" +#include "JavaUtils.h" + +#include "api/audio_codecs/builtin_audio_decoder_factory.h" + +JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioDecoderFactory_getBuiltinCodecs +(JNIEnv * env, jclass caller) +{ + try { + auto factory = webrtc::CreateBuiltinAudioDecoderFactory(); + + return jni::AudioCodecInfo::toJavaArray(env, factory->GetSupportedDecoders()).release(); + } + catch (...) { + ThrowCxxJavaException(env); + } + + return nullptr; +} diff --git a/webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioEncoderFactory.cpp b/webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioEncoderFactory.cpp new file mode 100644 index 000000000..1a36d8235 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/JNI_BuiltinAudioEncoderFactory.cpp @@ -0,0 +1,36 @@ +/* + * 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 "JNI_BuiltinAudioEncoderFactory.h" +#include "media/audio/codec/AudioCodecInfo.h" +#include "JavaUtils.h" + +#include "api/audio_codecs/builtin_audio_encoder_factory.h" + +JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_media_audio_codec_BuiltinAudioEncoderFactory_getBuiltinCodecs +(JNIEnv * env, jclass caller) +{ + try { + auto factory = webrtc::CreateBuiltinAudioEncoderFactory(); + + return jni::AudioCodecInfo::toJavaArray(env, factory->GetSupportedEncoders()).release(); + } + catch (...) { + ThrowCxxJavaException(env); + } + + return nullptr; +} diff --git a/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp b/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp index 4a9a4740b..3a604fd30 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp @@ -39,6 +39,7 @@ #include "api/ProxyAudioDeviceModule.h" #include "media/audio/CustomAudioSource.h" +#include "media/audio/codec/AudioCodecFactories.h" #include "media/video/codec/DefaultVideoCodecFactories.h" #include "media/video/codec/VideoDecoderFactoryWrapper.h" #include "media/video/codec/VideoEncoderFactoryWrapper.h" @@ -51,7 +52,8 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_initialize (JNIEnv * env, jobject caller, jobject jFieldTrials, jobject audioModule, jobject audioProcessing, - jobject jVideoEncoderFactory, jobject jVideoDecoderFactory) + jobject jVideoEncoderFactory, jobject jVideoDecoderFactory, jobject jAudioEncoderFactory, + jobject jAudioDecoderFactory) { webrtc::AudioDeviceModule * audioDevModule = (audioModule != nullptr) ? GetHandle(env, audioModule) @@ -86,6 +88,11 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_initialize ? std::make_unique(env, jVideoDecoderFactory) : jni::CreateDefaultVideoDecoderFactory(); + webrtc::scoped_refptr audioEncoderFactory = + jni::CreateAudioEncoderFactory(env, jni::JavaLocalRef(env, jAudioEncoderFactory)); + webrtc::scoped_refptr audioDecoderFactory = + jni::CreateAudioDecoderFactory(env, jni::JavaLocalRef(env, jAudioDecoderFactory)); + auto networkThread = webrtc::Thread::CreateWithSocketServer(); networkThread->SetName("webrtc_jni_network_thread", nullptr); @@ -141,8 +148,8 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_initialize workerThread.get(), signalingThread.get(), proxy, - webrtc::CreateBuiltinAudioEncoderFactory(), - webrtc::CreateBuiltinAudioDecoderFactory(), + audioEncoderFactory, + audioDecoderFactory, std::move(videoEncoderFactory), std::move(videoDecoderFactory), nullptr, diff --git a/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecFactories.cpp b/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecFactories.cpp new file mode 100644 index 000000000..98794d937 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecFactories.cpp @@ -0,0 +1,92 @@ +/* + * 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/audio/codec/AudioCodecFactories.h" +#include "media/audio/codec/FilteredAudioDecoderFactory.h" +#include "media/audio/codec/FilteredAudioEncoderFactory.h" +#include "Exception.h" +#include "JavaString.h" +#include "JavaUtils.h" + +#include "api/audio_codecs/builtin_audio_decoder_factory.h" +#include "api/audio_codecs/builtin_audio_encoder_factory.h" +#include "api/make_ref_counted.h" + +#include +#include + +namespace jni +{ + namespace + { + // Reads the canonical codec names a Java built-in factory holds. The + // built-in factories are the only subclasses Java allows, and both + // keep the names in the same field. + std::vector GetCodecNames(JNIEnv * env, const JavaRef & factory) + { + std::vector names; + + if (factory.get() == nullptr) { + return names; + } + + JavaLocalRef cls(env, env->GetObjectClass(factory.get())); + jfieldID field = env->GetFieldID(cls.get(), "codecNames", "[Ljava/lang/String;"); + + if (field == nullptr) { + env->ExceptionClear(); + throw Exception("The audio codec factory is not a built-in factory"); + } + + JavaLocalRef array(env, static_cast(env->GetObjectField(factory.get(), field))); + jsize count = (array.get() != nullptr) ? env->GetArrayLength(array.get()) : 0; + + for (jsize i = 0; i < count; i++) { + JavaLocalRef name(env, static_cast(env->GetObjectArrayElement(array.get(), i))); + + names.push_back(JavaString::toNative(env, name)); + } + + return names; + } + } + + webrtc::scoped_refptr CreateAudioEncoderFactory(JNIEnv * env, + const JavaRef & factory) + { + auto builtin = webrtc::CreateBuiltinAudioEncoderFactory(); + auto names = GetCodecNames(env, factory); + + if (names.empty()) { + return builtin; + } + + return webrtc::make_ref_counted(builtin, std::move(names)); + } + + webrtc::scoped_refptr CreateAudioDecoderFactory(JNIEnv * env, + const JavaRef & factory) + { + auto builtin = webrtc::CreateBuiltinAudioDecoderFactory(); + auto names = GetCodecNames(env, factory); + + if (names.empty()) { + return builtin; + } + + return webrtc::make_ref_counted(builtin, std::move(names)); + } +} diff --git a/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecInfo.cpp b/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecInfo.cpp new file mode 100644 index 000000000..693d6d90e --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecInfo.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/audio/codec/AudioCodecInfo.h" +#include "JavaClasses.h" +#include "JavaHashMap.h" +#include "JavaString.h" +#include "JavaUtils.h" +#include "JNI_WebRTC.h" + +namespace jni +{ + namespace AudioCodecInfo + { + JavaLocalRef toJava(JNIEnv * env, const webrtc::SdpAudioFormat & format) + { + const auto javaClass = JavaClasses::get(env); + + JavaHashMap parameters(env); + + for (const auto & [key, value] : format.parameters) { + parameters.put(JavaString::toJava(env, key), JavaString::toJava(env, value)); + } + + JavaLocalRef name = JavaString::toJava(env, format.name); + JavaLocalRef parameterMap = parameters; + + jobject info = env->NewObject(javaClass->cls, javaClass->ctor, name.get(), + static_cast(format.clockrate_hz), static_cast(format.num_channels), + parameterMap.get()); + + ExceptionCheck(env); + + return JavaLocalRef(env, info); + } + + JavaLocalRef toJavaArray(JNIEnv * env, const std::vector & specs) + { + const auto javaClass = JavaClasses::get(env); + + JavaLocalRef array(env, env->NewObjectArray(static_cast(specs.size()), javaClass->cls, nullptr)); + + ExceptionCheck(env); + + for (size_t i = 0; i < specs.size(); i++) { + JavaLocalRef info = toJava(env, specs[i].format); + + env->SetObjectArrayElement(array.get(), static_cast(i), info.get()); + } + + return array; + } + + JavaAudioCodecInfoClass::JavaAudioCodecInfoClass(JNIEnv * env) + { + cls = FindClass(env, PKG_AUDIO_CODEC"AudioCodecInfo"); + + ctor = GetMethod(env, cls, "", "(" STRING_SIG "II" MAP_SIG ")V"); + } + } +} diff --git a/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecSelection.cpp b/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecSelection.cpp new file mode 100644 index 000000000..82c71b8e9 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/media/audio/codec/AudioCodecSelection.cpp @@ -0,0 +1,49 @@ +/* + * 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/audio/codec/AudioCodecSelection.h" + +#include "absl/strings/match.h" + +namespace jni +{ + bool IsSelectedAudioCodec(const std::string & name, const std::vector & names) + { + for (const auto & selected : names) { + if (absl::EqualsIgnoreCase(name, selected)) { + return true; + } + } + + return false; + } + + std::vector SelectAudioCodecs(const std::vector & specs, + const std::vector & names) + { + std::vector selected; + + for (const auto & name : names) { + for (const auto & spec : specs) { + if (absl::EqualsIgnoreCase(spec.format.name, name)) { + selected.push_back(spec); + } + } + } + + return selected; + } +} diff --git a/webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioDecoderFactory.cpp b/webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioDecoderFactory.cpp new file mode 100644 index 000000000..1bc6bf1a2 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioDecoderFactory.cpp @@ -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. + */ + +#include "media/audio/codec/FilteredAudioDecoderFactory.h" +#include "media/audio/codec/AudioCodecSelection.h" + +#include + +namespace jni +{ + FilteredAudioDecoderFactory::FilteredAudioDecoderFactory(webrtc::scoped_refptr factory, + std::vector names) : + factory(std::move(factory)), + names(std::move(names)) + { + } + + std::vector FilteredAudioDecoderFactory::GetSupportedDecoders() + { + return SelectAudioCodecs(factory->GetSupportedDecoders(), names); + } + + bool FilteredAudioDecoderFactory::IsSupportedDecoder(const webrtc::SdpAudioFormat & format) + { + return IsSelectedAudioCodec(format.name, names) && factory->IsSupportedDecoder(format); + } + + std::unique_ptr FilteredAudioDecoderFactory::Create(const webrtc::Environment & env, + const webrtc::SdpAudioFormat & format) + { + if (!IsSelectedAudioCodec(format.name, names)) { + return nullptr; + } + + return factory->Create(env, format); + } +} diff --git a/webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioEncoderFactory.cpp b/webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioEncoderFactory.cpp new file mode 100644 index 000000000..e11945351 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/media/audio/codec/FilteredAudioEncoderFactory.cpp @@ -0,0 +1,54 @@ +/* + * 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/audio/codec/FilteredAudioEncoderFactory.h" +#include "media/audio/codec/AudioCodecSelection.h" + +#include + +namespace jni +{ + FilteredAudioEncoderFactory::FilteredAudioEncoderFactory(webrtc::scoped_refptr factory, + std::vector names) : + factory(std::move(factory)), + names(std::move(names)) + { + } + + std::vector FilteredAudioEncoderFactory::GetSupportedEncoders() + { + return SelectAudioCodecs(factory->GetSupportedEncoders(), names); + } + + std::optional FilteredAudioEncoderFactory::QueryAudioEncoder(const webrtc::SdpAudioFormat & format) + { + if (!IsSelectedAudioCodec(format.name, names)) { + return std::nullopt; + } + + return factory->QueryAudioEncoder(format); + } + + std::unique_ptr FilteredAudioEncoderFactory::Create(const webrtc::Environment & env, + const webrtc::SdpAudioFormat & format, Options options) + { + if (!IsSelectedAudioCodec(format.name, names)) { + return nullptr; + } + + return factory->Create(env, format, std::move(options)); + } +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java b/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java index b18994374..55a20e39e 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java @@ -26,6 +26,10 @@ import dev.onvoid.webrtc.media.audio.AudioTrackSource; import dev.onvoid.webrtc.media.audio.AudioTrack; import dev.onvoid.webrtc.media.audio.CustomAudioSource; +import dev.onvoid.webrtc.media.audio.codec.AudioDecoderFactory; +import dev.onvoid.webrtc.media.audio.codec.AudioEncoderFactory; +import dev.onvoid.webrtc.media.audio.codec.BuiltinAudioDecoderFactory; +import dev.onvoid.webrtc.media.audio.codec.BuiltinAudioEncoderFactory; import dev.onvoid.webrtc.media.video.VideoTrackSource; import dev.onvoid.webrtc.media.video.VideoTrack; import dev.onvoid.webrtc.media.video.codec.DefaultVideoDecoderFactory; @@ -110,7 +114,7 @@ private enum AudioInputMode { * Creates an instance of PeerConnectionFactory. */ public PeerConnectionFactory() { - initialize(null, null, null, null, null); + initialize(null, null, null, null, null, null, null); } /** @@ -120,7 +124,7 @@ public PeerConnectionFactory() { * @param audioProcessing The custom audio processing module. */ public PeerConnectionFactory(AudioProcessing audioProcessing) { - initialize(null, null, audioProcessing, null, null); + initialize(null, null, audioProcessing, null, null, null, null); } /** @@ -130,7 +134,7 @@ public PeerConnectionFactory(AudioProcessing audioProcessing) { * @param audioModule The custom audio device module. */ public PeerConnectionFactory(AudioDeviceModuleBase audioModule) { - initialize(null, audioModule, null, null, null); + initialize(null, audioModule, null, null, null, null, null); } /** @@ -142,7 +146,7 @@ public PeerConnectionFactory(AudioDeviceModuleBase audioModule) { */ public PeerConnectionFactory(AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing) { - initialize(null, audioModule, audioProcessing, null, null); + initialize(null, audioModule, audioProcessing, null, null, null, null); } /** @@ -154,7 +158,7 @@ public PeerConnectionFactory(AudioDeviceModuleBase audioModule, * non-null and non-empty. */ public PeerConnectionFactory(Map fieldTrials) { - initialize(fieldTrials, null, null, null, null); + initialize(fieldTrials, null, null, null, null, null, null); } /** @@ -168,7 +172,7 @@ public PeerConnectionFactory(Map fieldTrials) { */ public PeerConnectionFactory(Map fieldTrials, AudioProcessing audioProcessing) { - initialize(fieldTrials, null, audioProcessing, null, null); + initialize(fieldTrials, null, audioProcessing, null, null, null, null); } /** @@ -182,7 +186,7 @@ public PeerConnectionFactory(Map fieldTrials, */ public PeerConnectionFactory(Map fieldTrials, AudioDeviceModuleBase audioModule) { - initialize(fieldTrials, audioModule, null, null, null); + initialize(fieldTrials, audioModule, null, null, null, null, null); } /** @@ -197,7 +201,7 @@ public PeerConnectionFactory(Map fieldTrials, */ public PeerConnectionFactory(Map fieldTrials, AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing) { - initialize(fieldTrials, audioModule, audioProcessing, null, null); + initialize(fieldTrials, audioModule, audioProcessing, null, null, null, null); } /** @@ -216,7 +220,8 @@ private PeerConnectionFactory(Builder builder) { : builder.videoDecoderFactory; initialize(builder.fieldTrials, builder.audioModule, builder.audioProcessing, - encoderFactory, decoderFactory); + encoderFactory, decoderFactory, builder.audioEncoderFactory, + builder.audioDecoderFactory); } /** @@ -403,7 +408,9 @@ public RTCPeerConnection createPeerConnection(RTCConfiguration config, private native void initialize(Map fieldTrials, AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing, VideoEncoderFactory videoEncoderFactory, - VideoDecoderFactory videoDecoderFactory); + VideoDecoderFactory videoDecoderFactory, + AudioEncoderFactory audioEncoderFactory, + AudioDecoderFactory audioDecoderFactory); private native AudioTrackSource createAudioSourceInternal(AudioOptions options); @@ -460,6 +467,10 @@ public static final class Builder { private VideoDecoderFactory videoDecoderFactory; + private AudioEncoderFactory audioEncoderFactory; + + private AudioDecoderFactory audioDecoderFactory; + private Builder() { } @@ -539,6 +550,35 @@ public Builder setVideoDecoderFactory(VideoDecoderFactory factory) { return this; } + /** + * Sets the factory of the audio encoders, which decides what audio + * codecs the factory can send and in which order it offers them. + * Without one, the factory uses all codecs of a {@link + * BuiltinAudioEncoderFactory}. + * + * @param factory The audio encoder factory. + * + * @return This builder. + */ + public Builder setAudioEncoderFactory(AudioEncoderFactory factory) { + this.audioEncoderFactory = factory; + return this; + } + + /** + * Sets the factory of the audio decoders, which decides what audio + * codecs the factory can receive. Without one, the factory uses all + * codecs of a {@link BuiltinAudioDecoderFactory}. + * + * @param factory The audio decoder factory. + * + * @return This builder. + */ + public Builder setAudioDecoderFactory(AudioDecoderFactory factory) { + this.audioDecoderFactory = factory; + return this; + } + /** * Creates the PeerConnectionFactory. The builder may be used again * afterwards. diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecInfo.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecInfo.java new file mode 100644 index 000000000..93793a776 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecInfo.java @@ -0,0 +1,114 @@ +/* + * 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.audio.codec; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * An audio codec as an audio codec factory offers it in SDP: its name, clock + * rate, number of channels and format parameters, for example "opus" at 48000 + * Hz with 2 channels and {@code minptime=10;useinbandfec=1}. + * + * @author Alex Andres + */ +public final class AudioCodecInfo { + + private final String name; + + private final int clockRate; + + private final int channels; + + private final Map parameters; + + + /* + * Created by the native api. + */ + AudioCodecInfo(String name, int clockRate, int channels, + Map parameters) { + this.name = name; + this.clockRate = clockRate; + this.channels = channels; + this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters)); + } + + /** + * Returns the codec name, e.g. "opus", "G722", "PCMU" or "PCMA". + * + * @return The codec name. + */ + public String getName() { + return name; + } + + /** + * Returns the RTP clock rate in Hz. + * + * @return The clock rate. + */ + public int getClockRate() { + return clockRate; + } + + /** + * Returns the number of channels as signaled in SDP. + * + * @return The number of channels. + */ + public int getChannels() { + return channels; + } + + /** + * Returns the format parameters, the "a=fmtp" line of the codec. + * + * @return The format parameters, which cannot be modified. + */ + public Map getParameters() { + return parameters; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof AudioCodecInfo)) { + return false; + } + + AudioCodecInfo other = (AudioCodecInfo) obj; + + return clockRate == other.clockRate && channels == other.channels + && name.equalsIgnoreCase(other.name) + && parameters.equals(other.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name.toLowerCase(), clockRate, channels, parameters); + } + + @Override + public String toString() { + return String.format("%s/%d/%d %s", name, clockRate, channels, parameters); + } +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecSelection.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecSelection.java new file mode 100644 index 000000000..e226f3f5a --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioCodecSelection.java @@ -0,0 +1,109 @@ +/* + * 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.audio.codec; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Selects built-in codecs by name. The native factories filter and order the + * same way, so that what Java reports is what WebRTC offers. + */ +final class AudioCodecSelection { + + private AudioCodecSelection() { + } + + /** + * Validates the codec names and returns them in canonical form: as the + * built-in codecs spell them, without duplicates, in the given order. + * + * @param builtin The built-in codecs. + * @param names The names to select, compared ignoring case. + * + * @return The canonical names, empty to select all. + */ + static String[] canonicalNames(List builtin, String... names) { + Objects.requireNonNull(names, "Codec names are null"); + + Set canonical = new LinkedHashSet<>(); + + for (String name : names) { + Objects.requireNonNull(name, "Codec name is null"); + + String match = null; + + for (AudioCodecInfo codec : builtin) { + if (codec.getName().equalsIgnoreCase(name)) { + match = codec.getName(); + break; + } + } + + if (match == null) { + throw new IllegalArgumentException(String.format( + "Unknown audio codec '%s', the built-in codecs are %s", + name, builtinNames(builtin))); + } + + canonical.add(match); + } + + return canonical.toArray(new String[0]); + } + + /** + * Returns the built-in codecs with the given names, ordered by the names + * and, for one name, by their built-in order. + * + * @param builtin The built-in codecs. + * @param names Canonical names, empty to select all. + * + * @return The selected codecs, which cannot be modified. + */ + static List select(List builtin, String[] names) { + if (names.length == 0) { + return Collections.unmodifiableList(new ArrayList<>(builtin)); + } + + List selected = new ArrayList<>(); + + for (String name : names) { + for (AudioCodecInfo codec : builtin) { + if (codec.getName().equalsIgnoreCase(name)) { + selected.add(codec); + } + } + } + + return Collections.unmodifiableList(selected); + } + + private static Set builtinNames(List builtin) { + Set names = new LinkedHashSet<>(); + + for (AudioCodecInfo codec : builtin) { + names.add(codec.getName()); + } + + return names; + } +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioDecoderFactory.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioDecoderFactory.java new file mode 100644 index 000000000..afca2e8bb --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioDecoderFactory.java @@ -0,0 +1,43 @@ +/* + * 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.audio.codec; + +import java.util.List; + +/** + * Creates the audio decoders of a {@link dev.onvoid.webrtc.PeerConnectionFactory}, + * and so decides which audio codecs it can receive. Set one with {@link + * dev.onvoid.webrtc.PeerConnectionFactory.Builder#setAudioDecoderFactory}. + *

+ * The decoders are WebRTC's own; {@link BuiltinAudioDecoderFactory} is the + * implementation. + * + * @author Alex Andres + */ +public abstract class AudioDecoderFactory { + + AudioDecoderFactory() { + } + + /** + * Returns the codecs this factory can decode, in order of preference. + * + * @return The supported codecs, which cannot be modified. + */ + public abstract List getSupportedCodecs(); + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioEncoderFactory.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioEncoderFactory.java new file mode 100644 index 000000000..bc05b5f5b --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/AudioEncoderFactory.java @@ -0,0 +1,43 @@ +/* + * 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.audio.codec; + +import java.util.List; + +/** + * Creates the audio encoders of a {@link dev.onvoid.webrtc.PeerConnectionFactory}, + * and so decides which audio codecs it can send. Set one with {@link + * dev.onvoid.webrtc.PeerConnectionFactory.Builder#setAudioEncoderFactory}. + *

+ * The encoders are WebRTC's own; {@link BuiltinAudioEncoderFactory} is the + * implementation. + * + * @author Alex Andres + */ +public abstract class AudioEncoderFactory { + + AudioEncoderFactory() { + } + + /** + * Returns the codecs this factory offers, in order of preference. + * + * @return The supported codecs, which cannot be modified. + */ + public abstract List getSupportedCodecs(); + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioDecoderFactory.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioDecoderFactory.java new file mode 100644 index 000000000..c600feae0 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioDecoderFactory.java @@ -0,0 +1,73 @@ +/* + * 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.audio.codec; + +import dev.onvoid.webrtc.internal.NativeLoader; + +import java.util.Arrays; +import java.util.List; + +/** + * WebRTC's built-in audio decoders: Opus, G722, PCMU and PCMA. Without codec + * names it decodes all of them, which is what a {@link + * dev.onvoid.webrtc.PeerConnectionFactory} uses by default. With names it + * decodes only those codecs, so a connection cannot receive the others. + * + * @author Alex Andres + */ +public final class BuiltinAudioDecoderFactory extends AudioDecoderFactory { + + static { + try { + NativeLoader.loadLibrary("webrtc-java"); + } + catch (Exception e) { + throw new RuntimeException("Load library 'webrtc-java' failed", e); + } + } + + + /* Read natively when the PeerConnectionFactory is built. */ + private final String[] codecNames; + + private final List supportedCodecs; + + + /** + * Creates a factory with the given built-in codecs, or all of them when + * no names are given. + * + * @param codecNames The names of the codecs to decode, in order of + * preference, compared ignoring case. + * + * @throws IllegalArgumentException If a name is not a built-in codec. + */ + public BuiltinAudioDecoderFactory(String... codecNames) { + List builtin = Arrays.asList(getBuiltinCodecs()); + + this.codecNames = AudioCodecSelection.canonicalNames(builtin, codecNames); + this.supportedCodecs = AudioCodecSelection.select(builtin, this.codecNames); + } + + @Override + public List getSupportedCodecs() { + return supportedCodecs; + } + + private static native AudioCodecInfo[] getBuiltinCodecs(); + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioEncoderFactory.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioEncoderFactory.java new file mode 100644 index 000000000..e01fbe584 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/codec/BuiltinAudioEncoderFactory.java @@ -0,0 +1,83 @@ +/* + * 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.audio.codec; + +import dev.onvoid.webrtc.internal.NativeLoader; + +import java.util.Arrays; +import java.util.List; + +/** + * WebRTC's built-in audio encoders: Opus, G722, PCMU and PCMA. Without codec + * names it offers all of them, which is what a {@link + * dev.onvoid.webrtc.PeerConnectionFactory} uses by default. With names it + * offers only those codecs, in the given order, for example only Opus: + *

{@code
+ * PeerConnectionFactory factory = PeerConnectionFactory.builder()
+ *     .setAudioEncoderFactory(new BuiltinAudioEncoderFactory("opus"))
+ *     .build();
+ * }
+ *

+ * WebRTC adds the auxiliary codecs by itself, depending on the selected + * ones: "telephone-event" for DTMF at their clock rates of 8000 and 48000 Hz, + * "CN" for comfort noise next to 8000 Hz codecs, and "red" next to Opus. The + * {@link #getSupportedCodecs() supported codecs} do not list them. + * + * @author Alex Andres + */ +public final class BuiltinAudioEncoderFactory extends AudioEncoderFactory { + + static { + try { + NativeLoader.loadLibrary("webrtc-java"); + } + catch (Exception e) { + throw new RuntimeException("Load library 'webrtc-java' failed", e); + } + } + + + /* Read natively when the PeerConnectionFactory is built. */ + private final String[] codecNames; + + private final List supportedCodecs; + + + /** + * Creates a factory with the given built-in codecs, or all of them when + * no names are given. + * + * @param codecNames The names of the codecs to offer, in order of + * preference, compared ignoring case. + * + * @throws IllegalArgumentException If a name is not a built-in codec. + */ + public BuiltinAudioEncoderFactory(String... codecNames) { + List builtin = Arrays.asList(getBuiltinCodecs()); + + this.codecNames = AudioCodecSelection.canonicalNames(builtin, codecNames); + this.supportedCodecs = AudioCodecSelection.select(builtin, this.codecNames); + } + + @Override + public List getSupportedCodecs() { + return supportedCodecs; + } + + private static native AudioCodecInfo[] getBuiltinCodecs(); + +} diff --git a/webrtc/src/main/java/module-info.java b/webrtc/src/main/java/module-info.java index 778d8892d..2be00842b 100644 --- a/webrtc/src/main/java/module-info.java +++ b/webrtc/src/main/java/module-info.java @@ -9,6 +9,7 @@ exports dev.onvoid.webrtc.logging; exports dev.onvoid.webrtc.media; exports dev.onvoid.webrtc.media.audio; + exports dev.onvoid.webrtc.media.audio.codec; exports dev.onvoid.webrtc.media.video; exports dev.onvoid.webrtc.media.video.codec; exports dev.onvoid.webrtc.media.video.desktop; diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/AudioCodecFactoryTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/AudioCodecFactoryTests.java new file mode 100644 index 000000000..904773a52 --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/AudioCodecFactoryTests.java @@ -0,0 +1,202 @@ +/* + * 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.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 dev.onvoid.webrtc.media.MediaType; +import dev.onvoid.webrtc.media.audio.AudioDeviceModule; +import dev.onvoid.webrtc.media.audio.AudioLayer; +import dev.onvoid.webrtc.media.audio.CustomAudioSource; +import dev.onvoid.webrtc.media.audio.codec.AudioCodecInfo; +import dev.onvoid.webrtc.media.audio.codec.BuiltinAudioDecoderFactory; +import dev.onvoid.webrtc.media.audio.codec.BuiltinAudioEncoderFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +class AudioCodecFactoryTests { + + @Test + void builtinCodecs() { + List encoders = names(new BuiltinAudioEncoderFactory().getSupportedCodecs()); + List decoders = names(new BuiltinAudioDecoderFactory().getSupportedCodecs()); + + for (String codec : new String[] { "opus", "g722", "pcmu", "pcma" }) { + assertTrue(encoders.contains(codec), encoders.toString()); + assertTrue(decoders.contains(codec), decoders.toString()); + } + + AudioCodecInfo opus = new BuiltinAudioEncoderFactory("opus").getSupportedCodecs().get(0); + + assertEquals(48000, opus.getClockRate()); + assertEquals(2, opus.getChannels()); + } + + @Test + void selectionIsOrderedAndIgnoresCase() { + BuiltinAudioEncoderFactory factory = new BuiltinAudioEncoderFactory("PCMU", "OPUS", "pcmu"); + + assertEquals(Arrays.asList("pcmu", "opus"), names(factory.getSupportedCodecs())); + + assertThrows(UnsupportedOperationException.class, + () -> factory.getSupportedCodecs().clear()); + } + + @Test + void unknownCodecThrows() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> new BuiltinAudioEncoderFactory("opus", "AAC")); + + assertTrue(error.getMessage().contains("'AAC'"), error.getMessage()); + assertTrue(error.getMessage().contains("opus"), error.getMessage()); + + assertThrows(IllegalArgumentException.class, () -> new BuiltinAudioDecoderFactory("iLBC")); + assertThrows(NullPointerException.class, () -> new BuiltinAudioDecoderFactory((String) null)); + } + + @Test + void factoryOffersOnlySelectedCodecs() { + withFactory(new BuiltinAudioEncoderFactory("opus"), new BuiltinAudioDecoderFactory("PCMA"), factory -> { + List send = capabilityNames(factory.getRtpSenderCapabilities(MediaType.AUDIO)); + List receive = capabilityNames(factory.getRtpReceiverCapabilities(MediaType.AUDIO)); + + assertTrue(send.contains("opus"), send.toString()); + assertFalse(send.contains("pcmu") || send.contains("pcma") || send.contains("g722"), send.toString()); + + assertTrue(receive.contains("pcma"), receive.toString()); + assertFalse(receive.contains("opus") || receive.contains("pcmu") || receive.contains("g722"), receive.toString()); + + // WebRTC still adds DTMF at the selected codecs' clock rates. + assertTrue(send.contains("telephone-event"), send.toString()); + assertTrue(receive.contains("telephone-event"), receive.toString()); + }); + } + + @Test + void offerFollowsSelectedOrder() { + withFactory(new BuiltinAudioEncoderFactory("PCMU", "opus"), new BuiltinAudioDecoderFactory("PCMU", "opus"), factory -> { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), candidate -> { }); + + try { + connection.addTransceiver(factory.createAudioTrack("audio", new CustomAudioSource()), + new RTCRtpTransceiverInit()); + + TestCreateDescObserver observer = new TestCreateDescObserver(); + connection.createOffer(new RTCOfferOptions(), observer); + + String sdp = observer.get().sdp; + List codecs = offeredCodecs(sdp); + + assertEquals("pcmu", codecs.get(0), sdp); + assertEquals("opus", codecs.get(1), sdp); + assertFalse(codecs.contains("g722") || codecs.contains("pcma"), sdp); + } + catch (Exception e) { + throw new AssertionError(e); + } + finally { + connection.close(); + } + }); + } + + @Test + void callUsesSelectedCodec() { + withFactory(new BuiltinAudioEncoderFactory("PCMA"), new BuiltinAudioDecoderFactory("PCMA"), factory -> { + try (TestMediaCall call = new TestMediaCall(factory, false, true)) { + call.negotiate(); + call.awaitConnected(); + + List codecs = call.getAudioSender().getParameters().codecs.stream() + .map(codec -> codec.codecName.toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + + assertEquals("pcma", codecs.get(0), codecs.toString()); + assertFalse(codecs.contains("opus"), codecs.toString()); + } + catch (Exception e) { + throw new AssertionError(e); + } + }); + } + + private static void withFactory(BuiltinAudioEncoderFactory encoders, + BuiltinAudioDecoderFactory decoders, Consumer test) { + AudioDeviceModule audioModule = new AudioDeviceModule(AudioLayer.kDummyAudio); + PeerConnectionFactory factory = PeerConnectionFactory.builder() + .setAudioDeviceModule(audioModule) + .setAudioEncoderFactory(encoders) + .setAudioDecoderFactory(decoders) + .build(); + + try { + test.accept(factory); + } + finally { + factory.dispose(); + audioModule.dispose(); + } + } + + private static List names(List codecs) { + return codecs.stream() + .map(codec -> codec.getName().toLowerCase(Locale.ROOT)) + .distinct() + .collect(Collectors.toList()); + } + + private static List capabilityNames(RTCRtpCapabilities capabilities) { + return capabilities.getCodecs().stream() + .map(codec -> codec.getName().toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + } + + /** + * Returns the codec names of the audio media section, in payload order. + */ + private static List offeredCodecs(String sdp) { + List payloadTypes = new ArrayList<>(); + List codecs = new ArrayList<>(); + + for (String line : sdp.split("\r?\n")) { + if (line.startsWith("m=audio ")) { + String[] parts = line.split(" "); + payloadTypes.addAll(Arrays.asList(parts).subList(3, parts.length)); + } + } + for (String payloadType : payloadTypes) { + for (String line : sdp.split("\r?\n")) { + if (line.startsWith("a=rtpmap:" + payloadType + " ")) { + String name = line.substring(line.indexOf(' ') + 1, line.indexOf('/')); + codecs.add(name.toLowerCase(Locale.ROOT)); + } + } + } + + return codecs; + } +}