diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index 55a77a742..e6068529c 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -57,6 +57,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { text: 'Networking and ICE', collapsed: false, items: [ + { text: 'Peer Connection Configuration', link: '/networking/peer-connection-config' }, { text: 'Port Allocator Configuration', link: '/networking/port-allocator-config' }, ], }, diff --git a/docs/guide/index.md b/docs/guide/index.md index c478919ab..e848a6225 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -30,6 +30,7 @@ This section provides detailed guides for various features of the webrtc-java li ## Networking and ICE +- [Peer Connection Configuration](/guide/networking/peer-connection-config) - Candidate gathering, ICE checks, TURN, SRTP ciphers and other connection settings - [Port Allocator Config](/guide/networking/port-allocator-config) - Restrict ICE port ranges and control candidate gathering behavior ## Monitoring and Debugging diff --git a/docs/guide/networking/peer-connection-config.md b/docs/guide/networking/peer-connection-config.md new file mode 100644 index 000000000..f8e980b52 --- /dev/null +++ b/docs/guide/networking/peer-connection-config.md @@ -0,0 +1,85 @@ +# Peer Connection Configuration + +`RTCConfiguration` holds the settings a peer connection is created with: ICE servers and policies, how candidates are gathered, how ICE checks connections, and how media is protected. This guide covers the settings beyond the ICE servers, which most applications leave at their defaults, but which matter for fast connecting, restrictive networks, and mobile or multi-homed hosts. + +Every setting described here is unset by default, which keeps WebRTC's default. `getConfiguration()` returns the values in effect, defaults included. + +```java +RTCConfiguration config = new RTCConfiguration(); +config.iceServers.add(stunServer); +config.iceCandidatePoolSize = 2; +config.continualGatheringPolicy = RTCContinualGatheringPolicy.GATHER_CONTINUALLY; + +RTCPeerConnection peerConnection = factory.createPeerConnection(config, observer); +``` + +Some settings can be changed later with `setConfiguration()`, starting from `getConfiguration()`; those WebRTC does not allow to change make it throw. A configuration WebRTC rejects makes `createPeerConnection()` throw, with the reason in the message. + +## Candidate Gathering + +| Setting | Effect | +|---|---| +| `iceCandidatePoolSize` | Gathers this many candidates before a connection needs them, so that connecting is faster. Default 0. | +| `continualGatheringPolicy` | `GATHER_CONTINUALLY` keeps gathering after the first candidates, so that a network that comes up later gets candidates too, which lets a connection survive a network change. Default `GATHER_ONCE`. | +| `tcpCandidatePolicy` | `DISABLED` gathers no TCP candidates. Default `ENABLED`. | +| `candidateNetworkPolicy` | `LOW_COST` leaves out cellular networks. Default `ALL`. | +| `disableIpv6OnWifi`, `maxIpv6Networks` | Limit IPv6 candidates. Default: IPv6 on Wi-Fi allowed, at most 5 IPv6 networks. | +| `networkPreference` | A kind of network, e.g. `ETHERNET`, whose candidate pairs take precedence regardless of their priority. Default: none. | +| `vpnPreference` | Whether to use, avoid, prefer or require VPN connections. Default `DEFAULT`. | +| `surfaceIceCandidatesOnIceTransportTypeChanged` | Signals at once the candidates a change of `iceTransportPolicy` lets through. Default false. | + +To restrict ports and interfaces, see [Port Allocator Configuration](/guide/networking/port-allocator-config). + +## Connectivity Checks + +These tune how often ICE checks candidate pairs and when it gives up on one, all in milliseconds. Shorter intervals notice failures sooner, at the cost of more traffic. WebRTC checks that they fit together; for example, the check interval under strong connectivity may not exceed `stableWritableConnectionPingInterval`. + +`iceConnectionReceivingTimeout`, `iceBackupCandidatePairPingInterval`, `iceCheckIntervalStrongConnectivity`, `iceCheckIntervalWeakConnectivity`, `iceCheckMinInterval`, `iceUnwritableTimeout`, `iceUnwritableMinChecks` (a count), `iceInactiveTimeout`, `stunCandidateKeepaliveInterval`, `stableWritableConnectionPingInterval`. + +`prioritizeMostLikelyIceCandidatePairs` checks the pairs most likely to work first, usually those through TURN, and `enableIceRenomination` offers ICE renomination, which lets the controlling end switch the selected pair. + +## TURN + +| Setting | Effect | +|---|---| +| `presumeWritableWhenFullyRelayed` | Presumes TURN-to-TURN pairs work before a check succeeds, so that DTLS starts at once, which speeds up connecting through TURN. | +| `turnPortPrunePolicy` | Prunes TURN ports: `PRUNE_BASED_ON_PRIORITY` or `KEEP_FIRST_READY` per network. Default `NO_PRUNE`. | +| `turnLoggingId` | An identifier sent to TURN servers, to tie their logs to the application's. | + +## Media + +| Setting | Effect | +|---|---| +| `enableDscp` | Marks media packets with DSCP values. Default true. | +| `enableCpuAdaptation` | Lowers the resolution or frame rate of video when the CPU is overused. Default true. | +| `suspendBelowMinBitrate` | Stops sending video when the bitrate falls below its minimum. Default false. | +| `screencastMinBitrate` | The bitrate screen share video is padded up to, in kbps. Default 100. | + +## Security + +`cryptoOptions` selects the SRTP cipher suites a peer connection offers. A new `RTCCryptoOptions` holds WebRTC's defaults, to change from: + +```java +RTCCryptoOptions crypto = new RTCCryptoOptions(); +crypto.preferGcmCryptoSuites = true; +crypto.cryptexPolicy = RTCCryptexPolicy.NEGOTIATE; + +config.cryptoOptions = crypto; +``` + +`cryptexPolicy` encrypts the RTP header extensions and CSRCs as a whole (RFC 9335) where the peer supports it (`NEGOTIATE`) or always (`REQUIRE`). + +## Signaling + +| Setting | Effect | +|---|---| +| `offerExtmapAllowMixed` | Allows one- and two-byte header extensions to be mixed in offers. Default true. | +| `enableImplicitRollback` | Rolls a pending local offer back when a remote offer arrives, as perfect negotiation needs. Default false. | +| `alwaysNegotiateDataChannels` | Includes data channels in offers before any is created. Default false. | + +## Related API + +- `RTCConfiguration` — the configuration of a peer connection. +- `RTCPeerConnection.getConfiguration()`, `setConfiguration()` — read and change it. +- `RTCCryptoOptions` — the SRTP cipher suites. +- `PortAllocatorConfig` — ports and interfaces, see [Port Allocator Configuration](/guide/networking/port-allocator-config). diff --git a/webrtc-jni/src/main/cpp/include/api/RTCConfiguration.h b/webrtc-jni/src/main/cpp/include/api/RTCConfiguration.h index cc2c903ca..657b4e8a3 100644 --- a/webrtc-jni/src/main/cpp/include/api/RTCConfiguration.h +++ b/webrtc-jni/src/main/cpp/include/api/RTCConfiguration.h @@ -28,6 +28,9 @@ namespace jni { namespace RTCConfiguration { + // The position of an adapter type in RTCAdapterType. + enum class AdapterTypeOrdinal : int {}; + class JavaRTCConfigurationClass : public JavaClass { public: @@ -44,6 +47,38 @@ namespace jni jfieldID audioJitterBufferMaxPackets; jfieldID audioJitterBufferFastAccelerate; jfieldID audioJitterBufferMinDelayMs; + jfieldID iceCandidatePoolSize; + jfieldID tcpCandidatePolicy; + jfieldID candidateNetworkPolicy; + jfieldID continualGatheringPolicy; + jfieldID disableIpv6OnWifi; + jfieldID maxIpv6Networks; + jfieldID vpnPreference; + jfieldID surfaceIceCandidatesOnIceTransportTypeChanged; + jfieldID iceConnectionReceivingTimeout; + jfieldID iceBackupCandidatePairPingInterval; + jfieldID iceCheckIntervalStrongConnectivity; + jfieldID iceCheckIntervalWeakConnectivity; + jfieldID iceCheckMinInterval; + jfieldID iceUnwritableTimeout; + jfieldID iceUnwritableMinChecks; + jfieldID iceInactiveTimeout; + jfieldID stunCandidateKeepaliveInterval; + jfieldID stableWritableConnectionPingInterval; + jfieldID prioritizeMostLikelyIceCandidatePairs; + jfieldID enableIceRenomination; + jfieldID presumeWritableWhenFullyRelayed; + jfieldID turnPortPrunePolicy; + jfieldID enableDscp; + jfieldID enableCpuAdaptation; + jfieldID suspendBelowMinBitrate; + jfieldID screencastMinBitrate; + jfieldID offerExtmapAllowMixed; + jfieldID enableImplicitRollback; + jfieldID alwaysNegotiateDataChannels; + jfieldID networkPreference; + jfieldID turnLoggingId; + jfieldID cryptoOptions; }; JavaLocalRef toJava(JNIEnv * env, const webrtc::PeerConnectionInterface::RTCConfiguration & config); diff --git a/webrtc-jni/src/main/cpp/include/api/RTCCryptoOptions.h b/webrtc-jni/src/main/cpp/include/api/RTCCryptoOptions.h new file mode 100644 index 000000000..b4bea7140 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/api/RTCCryptoOptions.h @@ -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. + */ + +#ifndef JNI_WEBRTC_API_RTC_CRYPTO_OPTIONS_H_ +#define JNI_WEBRTC_API_RTC_CRYPTO_OPTIONS_H_ + +#include "JavaClass.h" +#include "JavaRef.h" + +#include "api/crypto/crypto_options.h" + +#include + +namespace jni +{ + namespace RTCCryptoOptions + { + class JavaRTCCryptoOptionsClass : public JavaClass + { + public: + explicit JavaRTCCryptoOptionsClass(JNIEnv * env); + + jclass cls; + jmethodID ctor; + jfieldID enableGcmCryptoSuites; + jfieldID preferGcmCryptoSuites; + jfieldID enableAes128Sha1_32CryptoCipher; + jfieldID enableAes128Sha1_80CryptoCipher; + jfieldID enableEncryptedRtpHeaderExtensions; + jfieldID cryptexPolicy; + }; + + JavaLocalRef toJava(JNIEnv * env, const webrtc::CryptoOptions & options); + + // Converts the SRTP options; the SFrame options, which this library + // does not expose, keep their defaults. + webrtc::CryptoOptions toNative(JNIEnv * env, const JavaRef & options); + } +} + +#endif diff --git a/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp b/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp index 4e3064d8f..fd1f34ed3 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_PeerConnectionFactory.cpp @@ -385,8 +385,15 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_createPee auto result = factory->CreatePeerConnectionOrError(configuration, std::move(dependencies)); if (!result.ok()) { + // No peer connection took the observer. + delete observer; + + // The type is a string_view, which is not terminated and cannot be + // passed through varargs as it is. + const std::string type(ToString(result.error().type())); + env->Throw(jni::JavaRuntimeException(env, "Create PeerConnection failed: %s %s", - ToString(result.error().type()), result.error().message())); + type.c_str(), result.error().message())); return nullptr; } @@ -401,6 +408,8 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_PeerConnectionFactory_createPee return javaPeerConnection.release(); } + delete observer; + return nullptr; } diff --git a/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp b/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp index 699cf584c..cbadf2a27 100644 --- a/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp +++ b/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp @@ -16,6 +16,7 @@ #include "WebRTCContext.h" #include "api/DataBufferFactory.h" +#include "api/RTCConfiguration.h" #include "api/RTCStats.h" #include "Exception.h" #include "JavaClassLoader.h" @@ -85,6 +86,13 @@ namespace jni JavaEnums::add(env, PKG"RTCSignalingState"); JavaEnums::add(env, PKG"TlsCertPolicy"); JavaEnums::add(env, PKG"RTCRtpTransceiverDirection"); + JavaEnums::add(env, PKG"RTCTcpCandidatePolicy"); + JavaEnums::add(env, PKG"RTCCandidateNetworkPolicy"); + JavaEnums::add(env, PKG"RTCContinualGatheringPolicy"); + JavaEnums::add(env, PKG"RTCPortPrunePolicy"); + JavaEnums::add(env, PKG"RTCVpnPreference"); + JavaEnums::add(env, PKG"RTCAdapterType"); + JavaEnums::add(env, PKG"RTCCryptexPolicy"); JavaEnums::add(env, PKG"RTCPriorityType"); JavaEnums::add(env, PKG"RTCDegradationPreference"); JavaEnums::add(env, PKG"RTCSdpType"); diff --git a/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp b/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp index 51e4f5a00..88f3cb994 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp @@ -15,6 +15,7 @@ */ #include "api/RTCConfiguration.h" +#include "api/RTCCryptoOptions.h" #include "api/RTCIceServer.h" #include "api/PortAllocatorConfig.h" #include "rtc/RTCCertificatePEM.h" @@ -25,6 +26,8 @@ #include "JavaList.h" #include "JavaRef.h" #include "JavaObject.h" +#include "JavaPrimitive.h" +#include "JavaString.h" #include "JavaUtils.h" #include "JNI_WebRTC.h" @@ -32,6 +35,40 @@ namespace jni { namespace RTCConfiguration { + namespace + { + // The order of RTCAdapterType. The native adapter types are bit + // flags, which the conversion of enums by ordinal cannot map. + const webrtc::AdapterType kAdapterTypes[] = { + webrtc::ADAPTER_TYPE_UNKNOWN, + webrtc::ADAPTER_TYPE_ETHERNET, + webrtc::ADAPTER_TYPE_WIFI, + webrtc::ADAPTER_TYPE_CELLULAR, + webrtc::ADAPTER_TYPE_VPN, + webrtc::ADAPTER_TYPE_LOOPBACK, + webrtc::ADAPTER_TYPE_ANY + }; + + AdapterTypeOrdinal AdapterTypeToOrdinal(webrtc::AdapterType type) + { + for (size_t i = 0; i < std::size(kAdapterTypes); i++) { + if (kAdapterTypes[i] == type) { + return static_cast(i); + } + } + + // The cellular subtypes of mobile platforms. + return static_cast(3); + } + + webrtc::AdapterType OrdinalToAdapterType(AdapterTypeOrdinal ordinal) + { + const size_t index = static_cast(ordinal); + + return index < std::size(kAdapterTypes) ? kAdapterTypes[index] : webrtc::ADAPTER_TYPE_UNKNOWN; + } + } + JavaLocalRef toJava(JNIEnv * env, const webrtc::PeerConnectionInterface::RTCConfiguration & nativeType) { const auto javaClass = JavaClasses::get(env); @@ -64,6 +101,67 @@ namespace jni env->SetBooleanField(config, javaClass->audioJitterBufferFastAccelerate, nativeType.audio_jitter_buffer_fast_accelerate); env->SetIntField(config, javaClass->audioJitterBufferMinDelayMs, nativeType.audio_jitter_buffer_min_delay_ms); + env->SetObjectField(config, javaClass->iceCandidatePoolSize, Integer::create(env, nativeType.ice_candidate_pool_size).get()); + env->SetObjectField(config, javaClass->tcpCandidatePolicy, JavaEnums::toJava(env, nativeType.tcp_candidate_policy).get()); + env->SetObjectField(config, javaClass->candidateNetworkPolicy, JavaEnums::toJava(env, nativeType.candidate_network_policy).get()); + env->SetObjectField(config, javaClass->continualGatheringPolicy, JavaEnums::toJava(env, nativeType.continual_gathering_policy).get()); + env->SetObjectField(config, javaClass->disableIpv6OnWifi, Boolean::create(env, nativeType.disable_ipv6_on_wifi).get()); + env->SetObjectField(config, javaClass->maxIpv6Networks, Integer::create(env, nativeType.max_ipv6_networks).get()); + env->SetObjectField(config, javaClass->vpnPreference, JavaEnums::toJava(env, nativeType.vpn_preference).get()); + env->SetObjectField(config, javaClass->surfaceIceCandidatesOnIceTransportTypeChanged, Boolean::create(env, nativeType.surface_ice_candidates_on_ice_transport_type_changed).get()); + if (nativeType.ice_connection_receiving_timeout != webrtc::PeerConnectionInterface::RTCConfiguration::kUndefined) { + env->SetObjectField(config, javaClass->iceConnectionReceivingTimeout, Integer::create(env, nativeType.ice_connection_receiving_timeout).get()); + } + if (nativeType.ice_backup_candidate_pair_ping_interval != webrtc::PeerConnectionInterface::RTCConfiguration::kUndefined) { + env->SetObjectField(config, javaClass->iceBackupCandidatePairPingInterval, Integer::create(env, nativeType.ice_backup_candidate_pair_ping_interval).get()); + } + if (nativeType.ice_check_interval_strong_connectivity.has_value()) { + env->SetObjectField(config, javaClass->iceCheckIntervalStrongConnectivity, Integer::create(env, *nativeType.ice_check_interval_strong_connectivity).get()); + } + if (nativeType.ice_check_interval_weak_connectivity.has_value()) { + env->SetObjectField(config, javaClass->iceCheckIntervalWeakConnectivity, Integer::create(env, *nativeType.ice_check_interval_weak_connectivity).get()); + } + if (nativeType.ice_check_min_interval.has_value()) { + env->SetObjectField(config, javaClass->iceCheckMinInterval, Integer::create(env, *nativeType.ice_check_min_interval).get()); + } + if (nativeType.ice_unwritable_timeout.has_value()) { + env->SetObjectField(config, javaClass->iceUnwritableTimeout, Integer::create(env, *nativeType.ice_unwritable_timeout).get()); + } + if (nativeType.ice_unwritable_min_checks.has_value()) { + env->SetObjectField(config, javaClass->iceUnwritableMinChecks, Integer::create(env, *nativeType.ice_unwritable_min_checks).get()); + } + if (nativeType.ice_inactive_timeout.has_value()) { + env->SetObjectField(config, javaClass->iceInactiveTimeout, Integer::create(env, *nativeType.ice_inactive_timeout).get()); + } + if (nativeType.stun_candidate_keepalive_interval.has_value()) { + env->SetObjectField(config, javaClass->stunCandidateKeepaliveInterval, Integer::create(env, *nativeType.stun_candidate_keepalive_interval).get()); + } + if (nativeType.stable_writable_connection_ping_interval_ms.has_value()) { + env->SetObjectField(config, javaClass->stableWritableConnectionPingInterval, Integer::create(env, *nativeType.stable_writable_connection_ping_interval_ms).get()); + } + env->SetObjectField(config, javaClass->prioritizeMostLikelyIceCandidatePairs, Boolean::create(env, nativeType.prioritize_most_likely_ice_candidate_pairs).get()); + env->SetObjectField(config, javaClass->enableIceRenomination, Boolean::create(env, nativeType.enable_ice_renomination).get()); + env->SetObjectField(config, javaClass->presumeWritableWhenFullyRelayed, Boolean::create(env, nativeType.presume_writable_when_fully_relayed).get()); + env->SetObjectField(config, javaClass->turnPortPrunePolicy, JavaEnums::toJava(env, nativeType.turn_port_prune_policy).get()); + env->SetObjectField(config, javaClass->enableDscp, Boolean::create(env, nativeType.media_config.enable_dscp).get()); + env->SetObjectField(config, javaClass->enableCpuAdaptation, Boolean::create(env, nativeType.media_config.video.enable_cpu_adaptation).get()); + env->SetObjectField(config, javaClass->suspendBelowMinBitrate, Boolean::create(env, nativeType.media_config.video.suspend_below_min_bitrate).get()); + if (nativeType.screencast_min_bitrate.has_value()) { + env->SetObjectField(config, javaClass->screencastMinBitrate, Integer::create(env, *nativeType.screencast_min_bitrate).get()); + } + env->SetObjectField(config, javaClass->offerExtmapAllowMixed, Boolean::create(env, nativeType.offer_extmap_allow_mixed).get()); + env->SetObjectField(config, javaClass->enableImplicitRollback, Boolean::create(env, nativeType.enable_implicit_rollback).get()); + env->SetObjectField(config, javaClass->alwaysNegotiateDataChannels, Boolean::create(env, nativeType.always_negotiate_data_channels).get()); + if (nativeType.network_preference.has_value()) { + env->SetObjectField(config, javaClass->networkPreference, + JavaEnums::toJava(env, AdapterTypeToOrdinal(*nativeType.network_preference)).get()); + } + if (!nativeType.turn_logging_id.empty()) { + env->SetObjectField(config, javaClass->turnLoggingId, JavaString::toJava(env, nativeType.turn_logging_id).get()); + } + + env->SetObjectField(config, javaClass->cryptoOptions, RTCCryptoOptions::toJava(env, nativeType.crypto_options).get()); + return JavaLocalRef(env, config); } @@ -111,6 +209,139 @@ namespace jni configuration.audio_jitter_buffer_max_packets = obj.getInt(javaClass->audioJitterBufferMaxPackets); configuration.audio_jitter_buffer_min_delay_ms = obj.getInt(javaClass->audioJitterBufferMinDelayMs); + JavaLocalRef iceCandidatePoolSize = obj.getObject(javaClass->iceCandidatePoolSize); + JavaLocalRef tcpCandidatePolicy = obj.getObject(javaClass->tcpCandidatePolicy); + JavaLocalRef candidateNetworkPolicy = obj.getObject(javaClass->candidateNetworkPolicy); + JavaLocalRef continualGatheringPolicy = obj.getObject(javaClass->continualGatheringPolicy); + JavaLocalRef disableIpv6OnWifi = obj.getObject(javaClass->disableIpv6OnWifi); + JavaLocalRef maxIpv6Networks = obj.getObject(javaClass->maxIpv6Networks); + JavaLocalRef vpnPreference = obj.getObject(javaClass->vpnPreference); + JavaLocalRef surfaceIceCandidatesOnIceTransportTypeChanged = obj.getObject(javaClass->surfaceIceCandidatesOnIceTransportTypeChanged); + JavaLocalRef iceConnectionReceivingTimeout = obj.getObject(javaClass->iceConnectionReceivingTimeout); + JavaLocalRef iceBackupCandidatePairPingInterval = obj.getObject(javaClass->iceBackupCandidatePairPingInterval); + JavaLocalRef iceCheckIntervalStrongConnectivity = obj.getObject(javaClass->iceCheckIntervalStrongConnectivity); + JavaLocalRef iceCheckIntervalWeakConnectivity = obj.getObject(javaClass->iceCheckIntervalWeakConnectivity); + JavaLocalRef iceCheckMinInterval = obj.getObject(javaClass->iceCheckMinInterval); + JavaLocalRef iceUnwritableTimeout = obj.getObject(javaClass->iceUnwritableTimeout); + JavaLocalRef iceUnwritableMinChecks = obj.getObject(javaClass->iceUnwritableMinChecks); + JavaLocalRef iceInactiveTimeout = obj.getObject(javaClass->iceInactiveTimeout); + JavaLocalRef stunCandidateKeepaliveInterval = obj.getObject(javaClass->stunCandidateKeepaliveInterval); + JavaLocalRef stableWritableConnectionPingInterval = obj.getObject(javaClass->stableWritableConnectionPingInterval); + JavaLocalRef prioritizeMostLikelyIceCandidatePairs = obj.getObject(javaClass->prioritizeMostLikelyIceCandidatePairs); + JavaLocalRef enableIceRenomination = obj.getObject(javaClass->enableIceRenomination); + JavaLocalRef presumeWritableWhenFullyRelayed = obj.getObject(javaClass->presumeWritableWhenFullyRelayed); + JavaLocalRef turnPortPrunePolicy = obj.getObject(javaClass->turnPortPrunePolicy); + JavaLocalRef enableDscp = obj.getObject(javaClass->enableDscp); + JavaLocalRef enableCpuAdaptation = obj.getObject(javaClass->enableCpuAdaptation); + JavaLocalRef suspendBelowMinBitrate = obj.getObject(javaClass->suspendBelowMinBitrate); + JavaLocalRef screencastMinBitrate = obj.getObject(javaClass->screencastMinBitrate); + JavaLocalRef offerExtmapAllowMixed = obj.getObject(javaClass->offerExtmapAllowMixed); + JavaLocalRef enableImplicitRollback = obj.getObject(javaClass->enableImplicitRollback); + JavaLocalRef alwaysNegotiateDataChannels = obj.getObject(javaClass->alwaysNegotiateDataChannels); + + if (iceCandidatePoolSize.get() != nullptr) { + configuration.ice_candidate_pool_size = Integer::getValue(env, iceCandidatePoolSize); + } + if (tcpCandidatePolicy.get() != nullptr) { + configuration.tcp_candidate_policy = JavaEnums::toNative(env, tcpCandidatePolicy); + } + if (candidateNetworkPolicy.get() != nullptr) { + configuration.candidate_network_policy = JavaEnums::toNative(env, candidateNetworkPolicy); + } + if (continualGatheringPolicy.get() != nullptr) { + configuration.continual_gathering_policy = JavaEnums::toNative(env, continualGatheringPolicy); + } + if (disableIpv6OnWifi.get() != nullptr) { + configuration.disable_ipv6_on_wifi = Boolean::getValue(env, disableIpv6OnWifi); + } + if (maxIpv6Networks.get() != nullptr) { + configuration.max_ipv6_networks = Integer::getValue(env, maxIpv6Networks); + } + if (vpnPreference.get() != nullptr) { + configuration.vpn_preference = JavaEnums::toNative(env, vpnPreference); + } + if (surfaceIceCandidatesOnIceTransportTypeChanged.get() != nullptr) { + configuration.surface_ice_candidates_on_ice_transport_type_changed = Boolean::getValue(env, surfaceIceCandidatesOnIceTransportTypeChanged); + } + if (iceConnectionReceivingTimeout.get() != nullptr) { + configuration.ice_connection_receiving_timeout = Integer::getValue(env, iceConnectionReceivingTimeout); + } + if (iceBackupCandidatePairPingInterval.get() != nullptr) { + configuration.ice_backup_candidate_pair_ping_interval = Integer::getValue(env, iceBackupCandidatePairPingInterval); + } + if (iceCheckIntervalStrongConnectivity.get() != nullptr) { + configuration.ice_check_interval_strong_connectivity = Integer::getValue(env, iceCheckIntervalStrongConnectivity); + } + if (iceCheckIntervalWeakConnectivity.get() != nullptr) { + configuration.ice_check_interval_weak_connectivity = Integer::getValue(env, iceCheckIntervalWeakConnectivity); + } + if (iceCheckMinInterval.get() != nullptr) { + configuration.ice_check_min_interval = Integer::getValue(env, iceCheckMinInterval); + } + if (iceUnwritableTimeout.get() != nullptr) { + configuration.ice_unwritable_timeout = Integer::getValue(env, iceUnwritableTimeout); + } + if (iceUnwritableMinChecks.get() != nullptr) { + configuration.ice_unwritable_min_checks = Integer::getValue(env, iceUnwritableMinChecks); + } + if (iceInactiveTimeout.get() != nullptr) { + configuration.ice_inactive_timeout = Integer::getValue(env, iceInactiveTimeout); + } + if (stunCandidateKeepaliveInterval.get() != nullptr) { + configuration.stun_candidate_keepalive_interval = Integer::getValue(env, stunCandidateKeepaliveInterval); + } + if (stableWritableConnectionPingInterval.get() != nullptr) { + configuration.stable_writable_connection_ping_interval_ms = Integer::getValue(env, stableWritableConnectionPingInterval); + } + if (prioritizeMostLikelyIceCandidatePairs.get() != nullptr) { + configuration.prioritize_most_likely_ice_candidate_pairs = Boolean::getValue(env, prioritizeMostLikelyIceCandidatePairs); + } + if (enableIceRenomination.get() != nullptr) { + configuration.enable_ice_renomination = Boolean::getValue(env, enableIceRenomination); + } + if (presumeWritableWhenFullyRelayed.get() != nullptr) { + configuration.presume_writable_when_fully_relayed = Boolean::getValue(env, presumeWritableWhenFullyRelayed); + } + if (turnPortPrunePolicy.get() != nullptr) { + configuration.turn_port_prune_policy = JavaEnums::toNative(env, turnPortPrunePolicy); + } + if (enableDscp.get() != nullptr) { + configuration.media_config.enable_dscp = Boolean::getValue(env, enableDscp); + } + if (enableCpuAdaptation.get() != nullptr) { + configuration.media_config.video.enable_cpu_adaptation = Boolean::getValue(env, enableCpuAdaptation); + } + if (suspendBelowMinBitrate.get() != nullptr) { + configuration.media_config.video.suspend_below_min_bitrate = Boolean::getValue(env, suspendBelowMinBitrate); + } + if (screencastMinBitrate.get() != nullptr) { + configuration.screencast_min_bitrate = Integer::getValue(env, screencastMinBitrate); + } + if (offerExtmapAllowMixed.get() != nullptr) { + configuration.offer_extmap_allow_mixed = Boolean::getValue(env, offerExtmapAllowMixed); + } + if (enableImplicitRollback.get() != nullptr) { + configuration.enable_implicit_rollback = Boolean::getValue(env, enableImplicitRollback); + } + if (alwaysNegotiateDataChannels.get() != nullptr) { + configuration.always_negotiate_data_channels = Boolean::getValue(env, alwaysNegotiateDataChannels); + } + + JavaLocalRef networkPreference = obj.getObject(javaClass->networkPreference); + JavaLocalRef turnLoggingId = obj.getString(javaClass->turnLoggingId); + JavaLocalRef cryptoOptions = obj.getObject(javaClass->cryptoOptions); + + if (networkPreference.get() != nullptr) { + configuration.network_preference = + OrdinalToAdapterType(JavaEnums::toNative(env, networkPreference)); + } + if (turnLoggingId.get() != nullptr) { + configuration.turn_logging_id = JavaString::toNative(env, turnLoggingId); + } + if (cryptoOptions.get() != nullptr) { + configuration.crypto_options = RTCCryptoOptions::toNative(env, cryptoOptions); + } + return configuration; } @@ -129,6 +360,38 @@ namespace jni audioJitterBufferMaxPackets = GetFieldID(env, cls, "audioJitterBufferMaxPackets", "I"); audioJitterBufferFastAccelerate = GetFieldID(env, cls, "audioJitterBufferFastAccelerate", "Z"); audioJitterBufferMinDelayMs = GetFieldID(env, cls, "audioJitterBufferMinDelayMs", "I"); + iceCandidatePoolSize = GetFieldID(env, cls, "iceCandidatePoolSize", INTEGER_SIG); + tcpCandidatePolicy = GetFieldID(env, cls, "tcpCandidatePolicy", "L" PKG "RTCTcpCandidatePolicy;"); + candidateNetworkPolicy = GetFieldID(env, cls, "candidateNetworkPolicy", "L" PKG "RTCCandidateNetworkPolicy;"); + continualGatheringPolicy = GetFieldID(env, cls, "continualGatheringPolicy", "L" PKG "RTCContinualGatheringPolicy;"); + disableIpv6OnWifi = GetFieldID(env, cls, "disableIpv6OnWifi", BOOLEAN_SIG); + maxIpv6Networks = GetFieldID(env, cls, "maxIpv6Networks", INTEGER_SIG); + vpnPreference = GetFieldID(env, cls, "vpnPreference", "L" PKG "RTCVpnPreference;"); + surfaceIceCandidatesOnIceTransportTypeChanged = GetFieldID(env, cls, "surfaceIceCandidatesOnIceTransportTypeChanged", BOOLEAN_SIG); + iceConnectionReceivingTimeout = GetFieldID(env, cls, "iceConnectionReceivingTimeout", INTEGER_SIG); + iceBackupCandidatePairPingInterval = GetFieldID(env, cls, "iceBackupCandidatePairPingInterval", INTEGER_SIG); + iceCheckIntervalStrongConnectivity = GetFieldID(env, cls, "iceCheckIntervalStrongConnectivity", INTEGER_SIG); + iceCheckIntervalWeakConnectivity = GetFieldID(env, cls, "iceCheckIntervalWeakConnectivity", INTEGER_SIG); + iceCheckMinInterval = GetFieldID(env, cls, "iceCheckMinInterval", INTEGER_SIG); + iceUnwritableTimeout = GetFieldID(env, cls, "iceUnwritableTimeout", INTEGER_SIG); + iceUnwritableMinChecks = GetFieldID(env, cls, "iceUnwritableMinChecks", INTEGER_SIG); + iceInactiveTimeout = GetFieldID(env, cls, "iceInactiveTimeout", INTEGER_SIG); + stunCandidateKeepaliveInterval = GetFieldID(env, cls, "stunCandidateKeepaliveInterval", INTEGER_SIG); + stableWritableConnectionPingInterval = GetFieldID(env, cls, "stableWritableConnectionPingInterval", INTEGER_SIG); + prioritizeMostLikelyIceCandidatePairs = GetFieldID(env, cls, "prioritizeMostLikelyIceCandidatePairs", BOOLEAN_SIG); + enableIceRenomination = GetFieldID(env, cls, "enableIceRenomination", BOOLEAN_SIG); + presumeWritableWhenFullyRelayed = GetFieldID(env, cls, "presumeWritableWhenFullyRelayed", BOOLEAN_SIG); + turnPortPrunePolicy = GetFieldID(env, cls, "turnPortPrunePolicy", "L" PKG "RTCPortPrunePolicy;"); + enableDscp = GetFieldID(env, cls, "enableDscp", BOOLEAN_SIG); + enableCpuAdaptation = GetFieldID(env, cls, "enableCpuAdaptation", BOOLEAN_SIG); + suspendBelowMinBitrate = GetFieldID(env, cls, "suspendBelowMinBitrate", BOOLEAN_SIG); + screencastMinBitrate = GetFieldID(env, cls, "screencastMinBitrate", INTEGER_SIG); + offerExtmapAllowMixed = GetFieldID(env, cls, "offerExtmapAllowMixed", BOOLEAN_SIG); + enableImplicitRollback = GetFieldID(env, cls, "enableImplicitRollback", BOOLEAN_SIG); + alwaysNegotiateDataChannels = GetFieldID(env, cls, "alwaysNegotiateDataChannels", BOOLEAN_SIG); + networkPreference = GetFieldID(env, cls, "networkPreference", "L" PKG "RTCAdapterType;"); + turnLoggingId = GetFieldID(env, cls, "turnLoggingId", STRING_SIG); + cryptoOptions = GetFieldID(env, cls, "cryptoOptions", "L" PKG "RTCCryptoOptions;"); } } } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/api/RTCCryptoOptions.cpp b/webrtc-jni/src/main/cpp/src/api/RTCCryptoOptions.cpp new file mode 100644 index 000000000..8dd48274d --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/api/RTCCryptoOptions.cpp @@ -0,0 +1,90 @@ +/* + * 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/RTCCryptoOptions.h" +#include "JavaClasses.h" +#include "JavaEnums.h" +#include "JavaObject.h" +#include "JavaUtils.h" +#include "JNI_WebRTC.h" + +namespace jni +{ + namespace RTCCryptoOptions + { + JavaLocalRef toJava(JNIEnv * env, const webrtc::CryptoOptions & options) + { + const auto javaClass = JavaClasses::get(env); + + jobject object = env->NewObject(javaClass->cls, javaClass->ctor); + + ExceptionCheck(env); + + env->SetBooleanField(object, javaClass->enableGcmCryptoSuites, options.srtp.enable_gcm_crypto_suites); + env->SetBooleanField(object, javaClass->preferGcmCryptoSuites, options.srtp.prefer_gcm_crypto_suites); + env->SetBooleanField(object, javaClass->enableAes128Sha1_32CryptoCipher, + options.srtp.enable_aes128_sha1_32_crypto_cipher); + env->SetBooleanField(object, javaClass->enableAes128Sha1_80CryptoCipher, + options.srtp.enable_aes128_sha1_80_crypto_cipher); + env->SetBooleanField(object, javaClass->enableEncryptedRtpHeaderExtensions, + options.srtp.enable_encrypted_rtp_header_extensions); + env->SetObjectField(object, javaClass->cryptexPolicy, + JavaEnums::toJava(env, options.srtp.cryptex_policy).get()); + + return JavaLocalRef(env, object); + } + + webrtc::CryptoOptions toNative(JNIEnv * env, const JavaRef & options) + { + const auto javaClass = JavaClasses::get(env); + + JavaObject obj(env, options); + + webrtc::CryptoOptions cryptoOptions; + cryptoOptions.srtp.enable_gcm_crypto_suites = obj.getBoolean(javaClass->enableGcmCryptoSuites); + cryptoOptions.srtp.prefer_gcm_crypto_suites = obj.getBoolean(javaClass->preferGcmCryptoSuites); + cryptoOptions.srtp.enable_aes128_sha1_32_crypto_cipher = + obj.getBoolean(javaClass->enableAes128Sha1_32CryptoCipher); + cryptoOptions.srtp.enable_aes128_sha1_80_crypto_cipher = + obj.getBoolean(javaClass->enableAes128Sha1_80CryptoCipher); + cryptoOptions.srtp.enable_encrypted_rtp_header_extensions = + obj.getBoolean(javaClass->enableEncryptedRtpHeaderExtensions); + + JavaLocalRef cryptexPolicy = obj.getObject(javaClass->cryptexPolicy); + + if (cryptexPolicy.get() != nullptr) { + cryptoOptions.srtp.cryptex_policy = + JavaEnums::toNative(env, cryptexPolicy); + } + + return cryptoOptions; + } + + JavaRTCCryptoOptionsClass::JavaRTCCryptoOptionsClass(JNIEnv * env) + { + cls = FindClass(env, PKG"RTCCryptoOptions"); + + ctor = GetMethod(env, cls, "", "()V"); + + enableGcmCryptoSuites = GetFieldID(env, cls, "enableGcmCryptoSuites", "Z"); + preferGcmCryptoSuites = GetFieldID(env, cls, "preferGcmCryptoSuites", "Z"); + enableAes128Sha1_32CryptoCipher = GetFieldID(env, cls, "enableAes128Sha1_32CryptoCipher", "Z"); + enableAes128Sha1_80CryptoCipher = GetFieldID(env, cls, "enableAes128Sha1_80CryptoCipher", "Z"); + enableEncryptedRtpHeaderExtensions = GetFieldID(env, cls, "enableEncryptedRtpHeaderExtensions", "Z"); + cryptexPolicy = GetFieldID(env, cls, "cryptexPolicy", "L" PKG "RTCCryptexPolicy;"); + } + } +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCAdapterType.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCAdapterType.java new file mode 100644 index 000000000..22a91cad0 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCAdapterType.java @@ -0,0 +1,62 @@ +/* + * 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; + +/** + * A kind of network adapter, as {@link RTCConfiguration#networkPreference} + * takes it. + * + * @author Alex Andres + */ +public enum RTCAdapterType { + + /** + * An adapter of unknown kind. + */ + UNKNOWN, + + /** + * A wired Ethernet adapter. + */ + ETHERNET, + + /** + * A Wi-Fi adapter. + */ + WIFI, + + /** + * A cellular adapter. + */ + CELLULAR, + + /** + * A VPN. + */ + VPN, + + /** + * The loopback adapter. + */ + LOOPBACK, + + /** + * An adapter bound to any address. + */ + ANY + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCCandidateNetworkPolicy.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCandidateNetworkPolicy.java new file mode 100644 index 000000000..e1e0cb8a5 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCandidateNetworkPolicy.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Which networks ICE gathers candidates on, as {@link + * RTCConfiguration#candidateNetworkPolicy} takes it. + * + * @author Alex Andres + */ +public enum RTCCandidateNetworkPolicy { + + /** + * All networks. + */ + ALL, + + /** + * Only networks that do not cost extra, leaving out cellular ones. + */ + LOW_COST + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCConfiguration.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCConfiguration.java index 001902374..456bb5887 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCConfiguration.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCConfiguration.java @@ -85,6 +85,203 @@ public class RTCConfiguration { */ public int audioJitterBufferMinDelayMs; + // The fields below are unset by default, which keeps WebRTC's default. + + /** + * How many ICE candidates to gather before a connection needs them, so + * that connecting is faster. If unset, 0: gathering starts with the + * connection. + */ + public Integer iceCandidatePoolSize; + + /** + * Whether ICE gathers TCP candidates. If unset, {@link + * RTCTcpCandidatePolicy#ENABLED}. + */ + public RTCTcpCandidatePolicy tcpCandidatePolicy; + + /** + * Which networks ICE gathers candidates on. If unset, {@link + * RTCCandidateNetworkPolicy#ALL}. + */ + public RTCCandidateNetworkPolicy candidateNetworkPolicy; + + /** + * Whether ICE keeps gathering candidates after the first ones. If unset, + * {@link RTCContinualGatheringPolicy#GATHER_ONCE}. + */ + public RTCContinualGatheringPolicy continualGatheringPolicy; + + /** + * Whether to leave out IPv6 on Wi-Fi networks. If unset, false. + */ + public Boolean disableIpv6OnWifi; + + /** + * The largest number of IPv6 networks to gather candidates on. If unset, + * WebRTC's default of 5. + */ + public Integer maxIpv6Networks; + + /** + * The kind of network ICE prefers: a candidate pair on it takes precedence + * over pairs on other networks, regardless of their priority or network + * cost. If unset, no preference. + */ + public RTCAdapterType networkPreference; + + /** + * How ICE treats VPN connections. If unset, {@link + * RTCVpnPreference#DEFAULT}. + */ + public RTCVpnPreference vpnPreference; + + /** + * Whether candidates that a change of {@link #iceTransportPolicy} lets + * through are signaled at once, rather than at the next gathering. If + * unset, false. + */ + public Boolean surfaceIceCandidatesOnIceTransportTypeChanged; + + /** + * How long a connection may go without receiving before it counts as not + * receiving, in milliseconds. If unset, WebRTC's default. + */ + public Integer iceConnectionReceivingTimeout; + + /** + * How often to ping a backup candidate pair, in milliseconds. If unset, + * WebRTC's default. + */ + public Integer iceBackupCandidatePairPingInterval; + + /** + * How often to check a candidate pair while connectivity is strong, in + * milliseconds. If unset, WebRTC's default. + */ + public Integer iceCheckIntervalStrongConnectivity; + + /** + * How often to check a candidate pair while connectivity is weak, in + * milliseconds. If unset, WebRTC's default. + */ + public Integer iceCheckIntervalWeakConnectivity; + + /** + * The shortest time between two checks of a candidate pair, in + * milliseconds. If unset, WebRTC's default. + */ + public Integer iceCheckMinInterval; + + /** + * How long a connection may go without a response before it counts as + * unwritable, in milliseconds. If unset, WebRTC's default. + */ + public Integer iceUnwritableTimeout; + + /** + * How many checks have to go unanswered before a connection counts as + * unwritable. If unset, WebRTC's default. + */ + public Integer iceUnwritableMinChecks; + + /** + * How long a connection may stay unwritable before it counts as inactive, + * in milliseconds. If unset, WebRTC's default. + */ + public Integer iceInactiveTimeout; + + /** + * How often to send STUN keepalives on a candidate, in milliseconds. If + * unset, WebRTC's default. + */ + public Integer stunCandidateKeepaliveInterval; + + /** + * How often to ping a connection that is writable and stable, in + * milliseconds. If unset, WebRTC's default. + */ + public Integer stableWritableConnectionPingInterval; + + /** + * Whether to check first the candidate pairs most likely to work, rather + * than going by priority alone. If unset, false. + */ + public Boolean prioritizeMostLikelyIceCandidatePairs; + + /** + * Whether to offer ICE renomination, which lets the controlling end switch + * the selected candidate pair, if both ends support it. If unset, false. + */ + public Boolean enableIceRenomination; + + /** + * Whether ICE presumes that TURN-to-TURN candidate pairs work before a + * check has succeeded, so that the DTLS handshake can start at once, which + * speeds up connecting through TURN. If unset, false. + */ + public Boolean presumeWritableWhenFullyRelayed; + + /** + * How ICE prunes TURN ports. If unset, {@link RTCPortPrunePolicy#NO_PRUNE}. + */ + public RTCPortPrunePolicy turnPortPrunePolicy; + + /** + * An identifier sent to TURN servers, which they can log to tie their + * sessions to an application's. If unset, none is sent. + */ + public String turnLoggingId; + + /** + * Whether to mark media packets with DSCP values, so that networks that + * honor them can prioritize them. If unset, true. + */ + public Boolean enableDscp; + + /** + * Whether video senders lower resolution or frame rate when the CPU is + * overused. If unset, true. + */ + public Boolean enableCpuAdaptation; + + /** + * Whether a video sender stops sending when the bitrate falls below its + * minimum, rather than sending at too low a quality. If unset, false. + */ + public Boolean suspendBelowMinBitrate; + + /** + * The bitrate screen share video is padded up to, in kbps, which helps + * when a static screen turns into motion. If unset, 100 kbps. + */ + public Integer screencastMinBitrate; + + /** + * The ciphers to offer for SRTP. If unset, WebRTC's defaults, which a new + * {@link RTCCryptoOptions} holds too. + */ + public RTCCryptoOptions cryptoOptions; + + /** + * Whether offers allow one- and two-byte RTP header extensions to be mixed + * (a=extmap-allow-mixed). If unset, true. + */ + public Boolean offerExtmapAllowMixed; + + /** + * Whether setting a remote offer while a local offer is pending rolls the + * local offer back implicitly, as perfect negotiation needs. If unset, + * false. + */ + public Boolean enableImplicitRollback; + + /** + * Whether offers include data channels, as the first m-section, before any + * data channel is created. If unset, false. + */ + public Boolean alwaysNegotiateDataChannels; + /** * Creates an instance of RTCConfiguration. */ diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCContinualGatheringPolicy.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCContinualGatheringPolicy.java new file mode 100644 index 000000000..e52f0877c --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCContinualGatheringPolicy.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * Whether ICE keeps gathering candidates after the first ones, as {@link + * RTCConfiguration#continualGatheringPolicy} takes it. + * + * @author Alex Andres + */ +public enum RTCContinualGatheringPolicy { + + /** + * Gather once, when ICE starts. + */ + GATHER_ONCE, + + /** + * Keep gathering, so that a network that comes up later, or a changed + * address, gets candidates too, which lets a connection survive a network + * change. + */ + GATHER_CONTINUALLY + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCCryptexPolicy.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCryptexPolicy.java new file mode 100644 index 000000000..13da87b7a --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCryptexPolicy.java @@ -0,0 +1,42 @@ +/* + * 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; + +/** + * Whether SRTP encrypts the RTP header extensions and CSRCs as a whole + * (cryptex, RFC 9335), as {@link RTCCryptoOptions#cryptexPolicy} takes it. + * + * @author Alex Andres + */ +public enum RTCCryptexPolicy { + + /** + * Do not use cryptex. + */ + DISABLED, + + /** + * Use cryptex if the peer supports it. + */ + NEGOTIATE, + + /** + * Use cryptex, and fail with a peer that does not support it. + */ + REQUIRE + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCCryptoOptions.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCryptoOptions.java new file mode 100644 index 000000000..209832e2c --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCryptoOptions.java @@ -0,0 +1,72 @@ +/* + * 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; + +/** + * The ciphers a peer connection offers to protect its media with, as {@link + * RTCConfiguration#cryptoOptions} takes them. A new instance holds WebRTC's + * defaults. + * + * @author Alex Andres + */ +public class RTCCryptoOptions { + + /** Whether to offer the AES-GCM SRTP cipher suites. Default true. */ + public boolean enableGcmCryptoSuites = true; + + /** + * Whether to prefer the AES-GCM cipher suites over the others offered. + * Default false. + */ + public boolean preferGcmCryptoSuites; + + /** + * Whether to offer AES_CM_128_HMAC_SHA1_32, a legacy cipher suite with a + * shorter authentication tag. Default false. + */ + public boolean enableAes128Sha1_32CryptoCipher; + + /** + * Whether to offer AES_CM_128_HMAC_SHA1_80. Default true. + */ + public boolean enableAes128Sha1_80CryptoCipher = true; + + /** + * Whether to encrypt the RTP header extensions that can be encrypted (RFC + * 6904). Default true. + */ + public boolean enableEncryptedRtpHeaderExtensions = true; + + /** + * Whether to encrypt the header extensions and CSRCs as a whole (cryptex). + * Default {@link RTCCryptexPolicy#DISABLED}. + */ + public RTCCryptexPolicy cryptexPolicy = RTCCryptexPolicy.DISABLED; + + + @Override + public String toString() { + return String.format("%s [enableGcmCryptoSuites=%s, preferGcmCryptoSuites=%s, " + + "enableAes128Sha1_32CryptoCipher=%s, enableAes128Sha1_80CryptoCipher=%s, " + + "enableEncryptedRtpHeaderExtensions=%s, cryptexPolicy=%s]", + RTCCryptoOptions.class.getSimpleName(), enableGcmCryptoSuites, + preferGcmCryptoSuites, enableAes128Sha1_32CryptoCipher, + enableAes128Sha1_80CryptoCipher, enableEncryptedRtpHeaderExtensions, + cryptexPolicy); + } + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCPortPrunePolicy.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCPortPrunePolicy.java new file mode 100644 index 000000000..22faba037 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCPortPrunePolicy.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; + +/** + * How ICE prunes TURN ports, as {@link RTCConfiguration#turnPortPrunePolicy} + * takes it. + * + * @author Alex Andres + */ +public enum RTCPortPrunePolicy { + + /** + * Keep all TURN ports. + */ + NO_PRUNE, + + /** + * Prune TURN ports of lower priority on the same network. + */ + PRUNE_BASED_ON_PRIORITY, + + /** + * Keep the first TURN port that is ready on each network, and prune the + * others. + */ + KEEP_FIRST_READY + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCTcpCandidatePolicy.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCTcpCandidatePolicy.java new file mode 100644 index 000000000..09bd7c46e --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCTcpCandidatePolicy.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Whether ICE gathers TCP candidates, as {@link + * RTCConfiguration#tcpCandidatePolicy} takes it. + * + * @author Alex Andres + */ +public enum RTCTcpCandidatePolicy { + + /** + * Gather TCP candidates as well as UDP ones. + */ + ENABLED, + + /** + * Gather no TCP candidates. + */ + DISABLED + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCVpnPreference.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCVpnPreference.java new file mode 100644 index 000000000..f2da01ff3 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCVpnPreference.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * How ICE treats VPN connections, as {@link RTCConfiguration#vpnPreference} + * takes it. + * + * @author Alex Andres + */ +public enum RTCVpnPreference { + + /** + * No preference. + */ + DEFAULT, + + /** + * Use only VPN connections. + */ + ONLY_USE_VPN, + + /** + * Never use VPN connections. + */ + NEVER_USE_VPN, + + /** + * Use a VPN connection where there is one. + */ + PREFER_VPN, + + /** + * Use a VPN connection only where there is no other. + */ + AVOID_VPN + +} diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java new file mode 100644 index 000000000..955bedb1b --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java @@ -0,0 +1,234 @@ +/* + * 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Tests the configuration fields of peer connections that the native side + * has to carry: that they reach WebRTC, come back, and take effect. + */ +@Execution(ExecutionMode.SAME_THREAD) +class RTCConfigurationFieldsTests extends TestBase { + + private static final long TIMEOUT_SECONDS = 10; + + + @Test + void nativeDefaults() { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), new Observer()); + + try { + RTCConfiguration config = connection.getConfiguration(); + + // Unset fields keep WebRTC's defaults, including those that are + // true, which a plain Java boolean would have turned off. + assertEquals(0, (int) config.iceCandidatePoolSize); + assertEquals(5, (int) config.maxIpv6Networks); + assertEquals(RTCTcpCandidatePolicy.ENABLED, config.tcpCandidatePolicy); + assertEquals(RTCCandidateNetworkPolicy.ALL, config.candidateNetworkPolicy); + assertEquals(RTCContinualGatheringPolicy.GATHER_ONCE, config.continualGatheringPolicy); + assertEquals(RTCPortPrunePolicy.NO_PRUNE, config.turnPortPrunePolicy); + assertEquals(RTCVpnPreference.DEFAULT, config.vpnPreference); + assertEquals(Boolean.TRUE, config.enableDscp); + assertEquals(Boolean.TRUE, config.enableCpuAdaptation); + assertEquals(Boolean.TRUE, config.offerExtmapAllowMixed); + assertEquals(Boolean.FALSE, config.enableImplicitRollback); + assertNull(config.networkPreference); + assertNull(config.iceCheckMinInterval); + assertNull(config.turnLoggingId); + + assertNotNull(config.cryptoOptions); + assertTrue(config.cryptoOptions.enableGcmCryptoSuites); + assertTrue(config.cryptoOptions.enableAes128Sha1_80CryptoCipher); + assertFalse(config.cryptoOptions.enableAes128Sha1_32CryptoCipher); + assertEquals(RTCCryptexPolicy.DISABLED, config.cryptoOptions.cryptexPolicy); + } + finally { + connection.close(); + } + } + + @Test + void fieldsRoundTrip() { + RTCConfiguration config = new RTCConfiguration(); + config.iceCandidatePoolSize = 2; + config.tcpCandidatePolicy = RTCTcpCandidatePolicy.DISABLED; + config.candidateNetworkPolicy = RTCCandidateNetworkPolicy.LOW_COST; + config.continualGatheringPolicy = RTCContinualGatheringPolicy.GATHER_CONTINUALLY; + config.disableIpv6OnWifi = true; + config.maxIpv6Networks = 2; + config.networkPreference = RTCAdapterType.WIFI; + config.vpnPreference = RTCVpnPreference.AVOID_VPN; + config.iceConnectionReceivingTimeout = 3000; + // No longer than the ping interval of stable connections, which + // WebRTC checks. + config.iceCheckIntervalStrongConnectivity = 2000; + config.iceCheckMinInterval = 100; + config.stunCandidateKeepaliveInterval = 15000; + config.presumeWritableWhenFullyRelayed = true; + config.turnPortPrunePolicy = RTCPortPrunePolicy.KEEP_FIRST_READY; + config.turnLoggingId = "session-42"; + config.enableDscp = false; + config.suspendBelowMinBitrate = true; + config.screencastMinBitrate = 200; + config.enableImplicitRollback = true; + + config.cryptoOptions = new RTCCryptoOptions(); + config.cryptoOptions.enableAes128Sha1_32CryptoCipher = true; + config.cryptoOptions.cryptexPolicy = RTCCryptexPolicy.NEGOTIATE; + + RTCPeerConnection connection = factory.createPeerConnection(config, new Observer()); + + try { + RTCConfiguration applied = connection.getConfiguration(); + + assertEquals(2, (int) applied.iceCandidatePoolSize); + assertEquals(RTCTcpCandidatePolicy.DISABLED, applied.tcpCandidatePolicy); + assertEquals(RTCCandidateNetworkPolicy.LOW_COST, applied.candidateNetworkPolicy); + assertEquals(RTCContinualGatheringPolicy.GATHER_CONTINUALLY, applied.continualGatheringPolicy); + assertEquals(Boolean.TRUE, applied.disableIpv6OnWifi); + assertEquals(2, (int) applied.maxIpv6Networks); + assertEquals(RTCAdapterType.WIFI, applied.networkPreference); + assertEquals(RTCVpnPreference.AVOID_VPN, applied.vpnPreference); + assertEquals(3000, (int) applied.iceConnectionReceivingTimeout); + assertEquals(2000, (int) applied.iceCheckIntervalStrongConnectivity); + assertEquals(100, (int) applied.iceCheckMinInterval); + assertEquals(15000, (int) applied.stunCandidateKeepaliveInterval); + assertEquals(Boolean.TRUE, applied.presumeWritableWhenFullyRelayed); + assertEquals(RTCPortPrunePolicy.KEEP_FIRST_READY, applied.turnPortPrunePolicy); + assertEquals("session-42", applied.turnLoggingId); + assertEquals(Boolean.FALSE, applied.enableDscp); + assertEquals(Boolean.TRUE, applied.suspendBelowMinBitrate); + assertEquals(200, (int) applied.screencastMinBitrate); + assertEquals(Boolean.TRUE, applied.enableImplicitRollback); + assertTrue(applied.cryptoOptions.enableAes128Sha1_32CryptoCipher); + assertEquals(RTCCryptexPolicy.NEGOTIATE, applied.cryptoOptions.cryptexPolicy); + } + finally { + connection.close(); + } + } + + @Test + void invalidConfigurationFailsReadably() { + RTCConfiguration config = new RTCConfiguration(); + config.iceCheckIntervalStrongConnectivity = 10000; + config.stableWritableConnectionPingInterval = 1000; + + RuntimeException error = assertThrows(RuntimeException.class, + () -> factory.createPeerConnection(config, new Observer())); + + // The error type is readable text, not the bytes of a pointer. + assertTrue(error.getMessage().matches("Create PeerConnection failed: [A-Z_]+ .+"), error.getMessage()); + } + + @Test + void setConfigurationApplies() { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), new Observer()); + + try { + RTCConfiguration config = connection.getConfiguration(); + config.iceCandidatePoolSize = 3; + config.iceCheckMinInterval = 250; + config.networkPreference = RTCAdapterType.ETHERNET; + + connection.setConfiguration(config); + + RTCConfiguration applied = connection.getConfiguration(); + + assertEquals(3, (int) applied.iceCandidatePoolSize); + assertEquals(250, (int) applied.iceCheckMinInterval); + assertEquals(RTCAdapterType.ETHERNET, applied.networkPreference); + } + finally { + connection.close(); + } + } + + @Test + void tcpCandidatesDisabled() throws Exception { + RTCConfiguration config = new RTCConfiguration(); + config.tcpCandidatePolicy = RTCTcpCandidatePolicy.DISABLED; + + Observer observer = new Observer(); + RTCPeerConnection connection = factory.createPeerConnection(config, observer); + + try { + // A data channel gives the offer something to gather candidates for. + RTCDataChannel channel = connection.createDataChannel("data", new RTCDataChannelInit()); + + TestCreateDescObserver createObserver = new TestCreateDescObserver(); + connection.createOffer(new RTCOfferOptions(), createObserver); + + TestSetDescObserver setObserver = new TestSetDescObserver(); + connection.setLocalDescription(createObserver.get(), setObserver); + setObserver.get(); + + assertTrue(observer.gathered.await(TIMEOUT_SECONDS, TimeUnit.SECONDS), "gathering did not complete"); + assertFalse(observer.candidates.isEmpty(), "no candidates gathered"); + + for (RTCIceCandidate candidate : observer.candidates) { + assertFalse(candidate.sdp.toLowerCase().contains(" tcp "), candidate.sdp); + } + + channel.close(); + channel.dispose(); + } + finally { + connection.close(); + } + } + + + + private static class Observer implements PeerConnectionObserver { + + final List candidates = new CopyOnWriteArrayList<>(); + + final CountDownLatch gathered = new CountDownLatch(1); + + + @Override + public void onIceCandidate(RTCIceCandidate candidate) { + candidates.add(candidate); + } + + @Override + public void onIceGatheringChange(RTCIceGatheringState state) { + if (state == RTCIceGatheringState.COMPLETE) { + gathered.countDown(); + } + } + + } + +}