Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
]
}

Expand Down
40 changes: 40 additions & 0 deletions docs/guide/advanced/field-trials.md
Original file line number Diff line number Diff line change
@@ -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<String, String>` of trial names to their assigned group to one of the `PeerConnectionFactory` constructors that accept `fieldTrials`:

```java
Map<String, String> 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`.
4 changes: 2 additions & 2 deletions webrtc-jni/src/main/cpp/include/JNI_PeerConnectionFactory.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions webrtc-jni/src/main/cpp/include/api/FieldTrialsView.h
Original file line number Diff line number Diff line change
@@ -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 <map>
#include <string>

namespace jni
{
// A minimal FieldTrialsView backed by a std::map<std::string, std::string>.
// 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<std::string, std::string> trials);
virtual ~FieldTrialsView() = default;

// webrtc::FieldTrialsView implementation.
std::string Lookup(absl::string_view key) const override;

private:
std::map<std::string, std::string> trials;
};
}

#endif
29 changes: 27 additions & 2 deletions webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -58,14 +60,35 @@
#include "api/video_codecs/video_encoder_factory.h"
#include "api/video_codecs/video_encoder_factory_template.h"

#include <map>

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<webrtc::AudioDeviceModule>(env, audioModule)
: nullptr;

try {
std::map<std::string, std::string> fieldTrialsMap;

if (jFieldTrials != nullptr) {
for (const auto & entry : jni::JavaHashMap(env, jni::JavaLocalRef<jobject>(env, jFieldTrials))) {
std::string key = jni::JavaString::toNative(env, jni::static_java_ref_cast<jstring>(env, entry.first));
std::string value = jni::JavaString::toNative(env, jni::static_java_ref_cast<jstring>(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<webrtc::FieldTrialsView> fieldTrials = fieldTrialsMap.empty()
? nullptr
: std::make_unique<jni::FieldTrialsView>(std::move(fieldTrialsMap));

auto networkThread = webrtc::Thread::CreateWithSocketServer();
networkThread->SetName("webrtc_jni_network_thread", nullptr);

Expand Down Expand Up @@ -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());
Expand Down
32 changes: 32 additions & 0 deletions webrtc-jni/src/main/cpp/src/api/FieldTrialsView.cpp
Original file line number Diff line number Diff line change
@@ -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<std::string, std::string> 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();
}
}
69 changes: 63 additions & 6 deletions webrtc/src/main/java/dev/onvoid/webrtc/PeerConnectionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,7 +63,7 @@ public class PeerConnectionFactory extends DisposableNativeObject {
* Creates an instance of PeerConnectionFactory.
*/
public PeerConnectionFactory() {
this(null, null);
initialize(null, null, null);
}

/**
Expand All @@ -71,7 +73,7 @@ public PeerConnectionFactory() {
* @param audioProcessing The custom audio processing module.
*/
public PeerConnectionFactory(AudioProcessing audioProcessing) {
initialize(null, audioProcessing);
initialize(null, null, audioProcessing);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> fieldTrials,
AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing) {
initialize(fieldTrials, audioModule, audioProcessing);
}

/**
Expand Down Expand Up @@ -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<String, String> fieldTrials,
AudioDeviceModuleBase audioModule, AudioProcessing audioProcessing);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -60,6 +63,33 @@ void createWithAudioProcessing() {
factory.dispose();
}

@Test
void createWithFieldTrials() {
AudioDeviceModule audioDevModule = new AudioDeviceModule(AudioLayer.kDummyAudio);
Map<String, String> 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<String, String> fieldTrials = Collections.singletonMap("WebRTC-Bar", "");

assertThrows(Error.class, () -> {
new PeerConnectionFactory(fieldTrials, audioDevModule);
});
}

@Test
void createPeerConnectionNullParams() {
assertThrows(NullPointerException.class, () -> {
Expand Down
Loading