diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index 402c6128..ed659fbf 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -66,6 +66,13 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { { text: 'Logging', link: '/monitoring/logging' }, ], }, + { + text: 'Advanced', + collapsed: false, + items: [ + { text: 'Field Trials', link: '/advanced/field-trials' }, + ], + }, ] } diff --git a/docs/guide/advanced/field-trials.md b/docs/guide/advanced/field-trials.md new file mode 100644 index 00000000..e92e52cd --- /dev/null +++ b/docs/guide/advanced/field-trials.md @@ -0,0 +1,40 @@ +# Field Trials + +WebRTC uses field trials to gate experimental features and tune internal behavior (e.g. congestion control, codec selection, message interleaving) without changing the public API. This guide explains how to enable them when creating a `PeerConnectionFactory`. + +Field trials are applied once, at `PeerConnectionFactory` creation, and take effect for everything created by that factory (peer connections, tracks, sources). Different `PeerConnectionFactory` instances in the same process may use different field trials. + +## Basic Usage + +Pass a `Map` of trial names to their assigned group to one of the `PeerConnectionFactory` constructors that accept `fieldTrials`: + +```java +Map fieldTrials = Map.of( + "WebRTC-Bar", "Enabled" +); + +PeerConnectionFactory factory = new PeerConnectionFactory(fieldTrials); +``` + +Field trials can be combined with a custom audio device module and/or audio processing module: + +```java +PeerConnectionFactory factory = new PeerConnectionFactory(fieldTrials, audioModule); +PeerConnectionFactory factory = new PeerConnectionFactory(fieldTrials, audioProcessing); +PeerConnectionFactory factory = new PeerConnectionFactory(fieldTrials, audioModule, audioProcessing); +``` + +## Constraints + +Each key and value must be non-null and non-empty. Passing a map that violates this constraint throws an exception when the `PeerConnectionFactory` is created. + +## Tips and Troubleshooting + +- Field trial names and accepted groups (e.g. `Enabled`, `Disabled`) are defined by the underlying WebRTC native code and change between WebRTC releases; consult the WebRTC source for the branch this library is built against to find currently available trials. +- Since field trials configure experimental or internal behavior, prefer enabling only the trials you specifically need, and re-verify them after upgrading to a new release of this library. + +## Related API + +- `PeerConnectionFactory` — the constructors accepting `fieldTrials` apply them for the lifetime of that factory. + +For the full API, see the JavaDoc for `PeerConnectionFactory`. diff --git a/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h b/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h index 91d0ad6c..c73fba14 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h +++ b/webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h @@ -66,10 +66,10 @@ extern "C" { /* * Class: dev_onvoid_webrtc_PeerConnectionFactory * Method: initialize - * Signature: (Ldev/onvoid/webrtc/media/audio/AudioDeviceModule;Ldev/onvoid/webrtc/media/audio/AudioProcessing;)V + * Signature: (Ljava/util/Map;Ldev/onvoid/webrtc/media/audio/AudioDeviceModuleBase;Ldev/onvoid/webrtc/media/audio/AudioProcessing;)V */ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_initialize - (JNIEnv *, jobject, jobject, jobject); + (JNIEnv *, jobject, jobject, jobject, jobject); #ifdef __cplusplus } diff --git a/webrtc-jni/src/main/cpp/include/api/FieldTrialsView.h b/webrtc-jni/src/main/cpp/include/api/FieldTrialsView.h new file mode 100644 index 00000000..6c53bf1b --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/api/FieldTrialsView.h @@ -0,0 +1,45 @@ +/* + * 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_API_FIELD_TRIALS_VIEW_H_ +#define JNI_WEBRTC_API_FIELD_TRIALS_VIEW_H_ + +#include "api/field_trials_view.h" + +#include +#include + +namespace jni +{ + // A minimal FieldTrialsView backed by a std::map. + // webrtc::FieldTrials (api/field_trials.h) is not used here because its + // translation unit is not part of the linked webrtc archive for every + // build configuration of this project. + class FieldTrialsView : public webrtc::FieldTrialsView + { + public: + explicit FieldTrialsView(std::map trials); + virtual ~FieldTrialsView() = default; + + // webrtc::FieldTrialsView implementation. + std::string Lookup(absl::string_view key) const override; + + private: + std::map trials; + }; +} + +#endif diff --git a/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp b/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp index 4e87abcd..96e69199 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp @@ -17,12 +17,14 @@ #include "JNI_PeerConnectionFactory.h" #include "api/AudioOptions.h" #include "api/CreateSessionDescriptionObserver.h" +#include "api/FieldTrialsView.h" #include "api/PeerConnectionObserver.h" #include "api/RTCConfiguration.h" #include "api/RTCRtpCapabilities.h" #include "JavaEnums.h" #include "JavaError.h" #include "JavaFactories.h" +#include "JavaHashMap.h" #include "JavaNullPointerException.h" #include "JavaRuntimeException.h" #include "JavaRef.h" @@ -58,14 +60,35 @@ #include "api/video_codecs/video_encoder_factory.h" #include "api/video_codecs/video_encoder_factory_template.h" +#include + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_initialize -(JNIEnv * env, jobject caller, jobject audioModule, jobject audioProcessing) +(JNIEnv * env, jobject caller, jobject jFieldTrials, jobject audioModule, jobject audioProcessing) { webrtc::AudioDeviceModule * audioDevModule = (audioModule != nullptr) ? GetHandle(env, audioModule) : nullptr; try { + std::map fieldTrialsMap; + + if (jFieldTrials != nullptr) { + for (const auto & entry : jni::JavaHashMap(env, jni::JavaLocalRef(env, jFieldTrials))) { + std::string key = jni::JavaString::toNative(env, jni::static_java_ref_cast(env, entry.first)); + std::string value = jni::JavaString::toNative(env, jni::static_java_ref_cast(env, entry.second)); + + if (key.empty() || value.empty()) { + throw jni::Exception("Invalid field trial entry: key and value must not be empty"); + } + + fieldTrialsMap.emplace(std::move(key), std::move(value)); + } + } + + std::unique_ptr fieldTrials = fieldTrialsMap.empty() + ? nullptr + : std::make_unique(std::move(fieldTrialsMap)); + auto networkThread = webrtc::Thread::CreateWithSocketServer(); networkThread->SetName("webrtc_jni_network_thread", nullptr); @@ -133,7 +156,9 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_initialize webrtc::Dav1dDecoderTemplateAdapter>>(), #endif nullptr, - apm); + apm, + nullptr, + std::move(fieldTrials)); if (factory != nullptr) { SetHandle(env, caller, factory.release()); diff --git a/webrtc-jni/src/main/cpp/src/api/FieldTrialsView.cpp b/webrtc-jni/src/main/cpp/src/api/FieldTrialsView.cpp new file mode 100644 index 00000000..0e626c7b --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/api/FieldTrialsView.cpp @@ -0,0 +1,32 @@ +/* + * 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 "api/FieldTrialsView.h" + +namespace jni +{ + FieldTrialsView::FieldTrialsView(std::map trials) : + trials(std::move(trials)) + { + } + + std::string FieldTrialsView::Lookup(absl::string_view key) const + { + auto it = trials.find(std::string(key)); + + return it != trials.end() ? it->second : std::string(); + } +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java b/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java index 5c5b1126..b02f857f 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java @@ -28,6 +28,8 @@ import dev.onvoid.webrtc.media.video.VideoTrackSource; import dev.onvoid.webrtc.media.video.VideoTrack; +import java.util.Map; + /** * The PeerConnectionFactory is the main entry point for a WebRTC application. * It provides factory methods for {@link RTCPeerConnection} and audio/video @@ -61,7 +63,7 @@ public class PeerConnectionFactory extends DisposableNativeObject { * Creates an instance of PeerConnectionFactory. */ public PeerConnectionFactory() { - this(null, null); + initialize(null, null, null); } /** @@ -71,7 +73,7 @@ public PeerConnectionFactory() { * @param audioProcessing The custom audio processing module. */ public PeerConnectionFactory(AudioProcessing audioProcessing) { - initialize(null, audioProcessing); + initialize(null, null, audioProcessing); } /** @@ -81,7 +83,7 @@ public PeerConnectionFactory(AudioProcessing audioProcessing) { * @param audioModule The custom audio device module. */ public PeerConnectionFactory(AudioDeviceModuleBase audioModule) { - initialize(audioModule, null); + initialize(null, audioModule, null); } /** @@ -93,7 +95,62 @@ public PeerConnectionFactory(AudioDeviceModuleBase audioModule) { */ public PeerConnectionFactory(AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing) { - initialize(audioModule, audioProcessing); + initialize(null, audioModule, audioProcessing); + } + + /** + * Creates an instance of PeerConnectionFactory with the provided field + * trials, which allow enabling experimental WebRTC features. + * + * @param fieldTrials The field trials to set, e.g. {@code + * {"WebRTC-Bar": "Enabled"}}. Keys and values must be + * non-null and non-empty. + */ + public PeerConnectionFactory(Map fieldTrials) { + initialize(fieldTrials, null, null); + } + + /** + * Creates an instance of PeerConnectionFactory with the provided field + * trials and audio processing module. + * + * @param fieldTrials The field trials to set, e.g. {@code + * {"WebRTC-Bar": "Enabled"}}. Keys and values must + * be non-null and non-empty. + * @param audioProcessing The custom audio processing module. + */ + public PeerConnectionFactory(Map fieldTrials, + AudioProcessing audioProcessing) { + initialize(fieldTrials, null, audioProcessing); + } + + /** + * Creates an instance of PeerConnectionFactory with the provided field + * trials and audio device module. + * + * @param fieldTrials The field trials to set, e.g. {@code + * {"WebRTC-Bar": "Enabled"}}. Keys and values must be + * non-null and non-empty. + * @param audioModule The custom audio device module. + */ + public PeerConnectionFactory(Map fieldTrials, + AudioDeviceModuleBase audioModule) { + initialize(fieldTrials, audioModule, null); + } + + /** + * Creates an instance of PeerConnectionFactory with the provided field + * trials and modules for audio devices and audio processing. + * + * @param fieldTrials The field trials to set, e.g. {@code + * {"WebRTC-Bar": "Enabled"}}. Keys and values must + * be non-null and non-empty. + * @param audioModule The custom audio device module. + * @param audioProcessing The custom audio processing module. + */ + public PeerConnectionFactory(Map fieldTrials, + AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing) { + initialize(fieldTrials, audioModule, audioProcessing); } /** @@ -169,7 +226,7 @@ public native RTCPeerConnection createPeerConnection( @Override public native void dispose(); - private native void initialize(AudioDeviceModuleBase audioModule, - AudioProcessing audioProcessing); + private native void initialize(Map fieldTrials, + AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing); } diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/PeerConnectionFactoryTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/PeerConnectionFactoryTests.java index 8f78711e..467c89ca 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/PeerConnectionFactoryTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/PeerConnectionFactoryTests.java @@ -25,6 +25,9 @@ import dev.onvoid.webrtc.media.video.VideoDeviceSource; import dev.onvoid.webrtc.media.video.VideoTrack; +import java.util.Collections; +import java.util.Map; + import org.junit.jupiter.api.Test; class PeerConnectionFactoryTests extends TestBase { @@ -60,6 +63,33 @@ void createWithAudioProcessing() { factory.dispose(); } + @Test + void createWithFieldTrials() { + AudioDeviceModule audioDevModule = new AudioDeviceModule(AudioLayer.kDummyAudio); + Map fieldTrials = Collections.singletonMap("WebRTC-Bar", "Enabled"); + + PeerConnectionFactory factory = new PeerConnectionFactory(fieldTrials, audioDevModule); + factory.dispose(); + } + + @Test + void createWithEmptyFieldTrials() { + AudioDeviceModule audioDevModule = new AudioDeviceModule(AudioLayer.kDummyAudio); + + PeerConnectionFactory factory = new PeerConnectionFactory(Collections.emptyMap(), audioDevModule); + factory.dispose(); + } + + @Test + void createWithInvalidFieldTrials() { + AudioDeviceModule audioDevModule = new AudioDeviceModule(AudioLayer.kDummyAudio); + Map fieldTrials = Collections.singletonMap("WebRTC-Bar", ""); + + assertThrows(Error.class, () -> { + new PeerConnectionFactory(fieldTrials, audioDevModule); + }); + } + @Test void createPeerConnectionNullParams() { assertThrows(NullPointerException.class, () -> {