Skip to content

Commit d2e190b

Browse files
committed
feat: Enhance TURN relay session handling and ICE transport policies
- Introduced `iceTransportPolicy` to `RoomMediaStartupConfiguration` to control candidate selection based on transport type (all or relay). - Updated candidate selection logic to respect the new `iceTransportPolicy`, allowing suppression of host candidates when using relay-only TURN. - Added support for TCP and TLS endpoints in `TURNRelaySessionConfiguration`, enabling fallback to TCP when UDP endpoints are unavailable. - Implemented `TURNStreamTransport` for handling TCP/TLS connections, including sending and receiving data over network streams. - Created integration tests for TURN-only media startup scenarios and validated behavior with credentialed TURN relays. - Enhanced documentation to reflect changes in TURN relay session orchestration and ICE candidate planning.
1 parent e2a8433 commit d2e190b

11 files changed

Lines changed: 944 additions & 59 deletions

File tree

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,10 @@ maintenance execution, due action scheduling, relay ICE candidate planning,
213213
default UDP TURN relay allocation through the bound Room ICE socket, ChannelData
214214
relay bindings for relayed ICE checks/media datagrams, and TURN relay session
215215
configuration selection plus UDP/TCP/TLS fallback planning from parsed ICE
216-
server endpoints, plus bounded TURN relay session orchestration and
217-
deterministic setup-plan execution over abstract transports, peer
216+
server endpoints, plus bounded TURN relay session orchestration,
217+
deterministic setup-plan execution over abstract transports, TCP/TLS stream
218+
TURN setup execution, and relay-only ICE candidate policy for TURN-only
219+
validation, peer
218220
negotiation state reset across
219221
fresh join/reconnect/disconnect boundaries, RTCP
220222
sender/receiver report and bounded PLI/NACK subscriber feedback planning,
@@ -281,8 +283,8 @@ latest-value Room state and emitted as typed room events.
281283
The active implementation focus is now `1.0.0` hardening: expanding the new
282284
opt-in LiveKit-validated OpenSSL DTLS-SRTP publisher/subscriber media startup
283285
coverage, including selected ICE pair and default media/data session assertions,
284-
into full secure RTP/RTCP send/receive validation, TURN TCP/TLS, live
285-
quality-control wiring, real-device video display hardening,
286+
into full secure RTP/RTCP send/receive validation, live quality-control wiring,
287+
CI-gated TURN-only validation, real-device video display hardening,
286288
default-path DTLS-SCTP association receive-pump integration, LiveKit-validated
287289
data-channel recovery, hardening the default-gated live data-packet
288290
publish/receive smoke once standards SCTP is fully complete, real-device audio
@@ -401,9 +403,9 @@ configuration through `RoomOptions.automaticallyConfigureAudioSession`, and matc
401403
`RequestResponse` failure
402404
mapping are unit-tested, while LiveKit E2E media validation, real-device video
403405
display validation, standards-compliant live SCTP association behavior, TURN
404-
TCP/TLS execution, actual multi-layer simulcast/SVC media production, media
405-
recovery, route/interruption audio recovery, and end-to-end LiveKit hardening
406-
are still open.
406+
maintenance loop binding in default Room, CI-gated TURN-only validation, actual
407+
multi-layer simulcast/SVC media production, media recovery, route/interruption
408+
audio recovery, and end-to-end LiveKit hardening are still open.
407409

408410
## Benchmarks
409411

@@ -491,6 +493,11 @@ are included in the default integration gate whenever
491493
`LIVEKIT_NATIVE_RUN_INTEGRATION=1` is set. Strict production release mode now
492494
requires those integration variables so the future `productionReady` marker
493495
cannot pass while live tests are silently skipped.
496+
TURN-only media startup validation is additionally gated by
497+
`LIVEKIT_NATIVE_RUN_TURN_ONLY=1` and requires a LiveKit server that advertises
498+
credentialed TURN relay ICE servers; the test uses
499+
`RoomOptions.iceTransportPolicy = .relay` to suppress host and
500+
server-reflexive candidates.
494501

495502
## Requirements
496503

Sources/LiveKitNative/Core/Options.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import Foundation
22

3+
public enum ICETransportPolicy: String, Equatable, Sendable {
4+
case all
5+
case relay
6+
}
7+
38
public struct RoomOptions: Equatable, Sendable {
49
public var defaultAutoSubscribe: Bool
510
public var defaultAdaptiveStream: Bool?
@@ -11,6 +16,7 @@ public struct RoomOptions: Equatable, Sendable {
1116
public var automaticallyPlaySubscriberAudio: Bool
1217
public var automaticallyConfigureAudioSession: Bool
1318
public var audioSessionConfiguration: AudioSessionConfiguration
19+
public var iceTransportPolicy: ICETransportPolicy
1420

1521
public init(
1622
defaultAutoSubscribe: Bool = true,
@@ -22,7 +28,8 @@ public struct RoomOptions: Equatable, Sendable {
2228
automaticallyDecodeSubscriberVideo: Bool = false,
2329
automaticallyPlaySubscriberAudio: Bool = false,
2430
automaticallyConfigureAudioSession: Bool = false,
25-
audioSessionConfiguration: AudioSessionConfiguration = .voiceChat
31+
audioSessionConfiguration: AudioSessionConfiguration = .voiceChat,
32+
iceTransportPolicy: ICETransportPolicy = .all
2633
) {
2734
self.defaultAutoSubscribe = defaultAutoSubscribe
2835
self.defaultAdaptiveStream = defaultAdaptiveStream
@@ -34,6 +41,7 @@ public struct RoomOptions: Equatable, Sendable {
3441
self.automaticallyPlaySubscriberAudio = automaticallyPlaySubscriberAudio
3542
self.automaticallyConfigureAudioSession = automaticallyConfigureAudioSession
3643
self.audioSessionConfiguration = audioSessionConfiguration
44+
self.iceTransportPolicy = iceTransportPolicy
3745
}
3846
}
3947

Sources/LiveKitNative/Core/ProductionReadiness.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public extension LiveKitNative {
2929
status: .developerPreview,
3030
blockers: [
3131
"ICEAgent nomination, local ICE trickle signaling, server ICE server configuration application, STUN UDP server-reflexive candidate gathering, bound local ICE UDP host candidate gathering and socket reuse for STUN checks and media datagrams, default Room socket-backed subscriber/publisher media-data startup configuration, OpenSSL-backed WebRTC DTLS-SRTP use_srtp handshake/exporter wiring, DTLS-SRTP packet protection, RFC 5764 DTLS/SRTP datagram demux, shared persistent DTLS application-data plus SRTP session binding, SDP DTLS role/fingerprint extraction, handshaker-backed media-session binding with result role/profile validation, injected Room publisher/subscriber media startup, injected selected-pair ICE consent freshness loop with transport closure on expiry, injected publisher transport teardown, Room publisher RTP/RTCP and subscriber RTCP bridges through injected secure media transport, publisher and subscriber RTCP receive-handler loops, deterministic NACK/PLI RTCP feedback policy primitives, bounded subscriber feedback planning from H.264/VP8 RTP loss signals, Room subscriber feedback dispatch through injected subscriber RTCP transport, bounded RTP jitter-buffer primitives, stateful Opus/H.264 publisher RTP packetizer bridges, native camera/microphone publisher capture pipelines, Room publisher RTP sender registry lifecycle coverage, and subscriber RTP jitter-buffer/feedback dispatch exist, but production readiness still needs LiveKit server E2E secure RTP/RTCP integration, default TURN relay execution, Apple-platform OpenSSL packaging validation, and the remaining media pipeline gates.",
32-
"Fresh join, resume reconnect, disconnect, and failed signal-loop boundaries regenerate local ICE credentials, clear stale peer negotiation state, parse TURN UDP/TCP/TLS endpoint configuration, order TURN relay fallback candidates as UDP, TCP, then TLS while identifying the currently executable UDP datagram path, select supported TURN UDP relay session configurations from parsed ICE server endpoints with credentials, exercise TURN Allocate, Refresh, CreatePermission, and ChannelBind request/authentication/response-validation primitives with one-shot stale nonce retry, cover TURN ChannelData framing, provide deterministic allocation/permission maintenance scheduling and execution, plan relayed ICE candidates from TURN bindings, compose and execute a bounded TURN relay session setup plan over abstract transports, expose a ChannelData relay transport over an abstract media datagram transport, allocate default UDP TURN relay candidates through the bound Room ICE socket, use ChannelData relay bindings for relayed ICE checks and media datagrams, provide deterministic ICE consent freshness planning plus an injected Room startup loop, run ICEAgent connectivity checks through paced scheduling with queued triggered-check priority plus role-conflict role switching and pair-priority recompute, and rebuild reconnect SyncState SDP/trickle state with fresh local ICE credentials, but TCP/TLS TURN transport execution, LiveKit TURN-only validation, and live media recovery are not complete.",
32+
"Fresh join, resume reconnect, disconnect, and failed signal-loop boundaries regenerate local ICE credentials, clear stale peer negotiation state, parse TURN UDP/TCP/TLS endpoint configuration, order TURN relay fallback candidates as UDP, TCP, then TLS, select supported TURN relay session configurations from parsed ICE server endpoints with credentials, exercise TURN Allocate, Refresh, CreatePermission, and ChannelBind request/authentication/response-validation primitives with one-shot stale nonce retry, cover TURN ChannelData framing, provide deterministic allocation/permission maintenance scheduling and execution, plan relayed ICE candidates from TURN bindings, compose and execute bounded TURN relay session setup plans over UDP datagram and TCP/TLS stream transports, expose ChannelData relay transports over abstract media datagram transports, allocate default UDP TURN relay candidates through the bound Room ICE socket, use ChannelData relay bindings for relayed ICE checks and media datagrams, provide relay-only ICE candidate policy plus an opt-in LiveKit TURN-only validation harness, provide deterministic ICE consent freshness planning plus an injected Room startup loop, run ICEAgent connectivity checks through paced scheduling with queued triggered-check priority plus role-conflict role switching and pair-priority recompute, and rebuild reconnect SyncState SDP/trickle state with fresh local ICE credentials, but TURN maintenance loop binding in default Room, CI-gated TURN-only validation, and live media recovery are not complete.",
3333
"Publisher camera capture can encode H.264 through VideoToolbox with bounded frame backpressure/drop control and publisher microphone capture can encode Opus through AudioToolbox before RTP/SRTP send, while subscriber RTP can pass through jitter buffering, H.264/Opus packet assembly, NACK/PLI feedback, opt-in Opus decode-to-audio-playout scheduling, opt-in H.264 VideoToolbox decode-to-pixel-buffer scheduling, application-provided subscriber video renderer handoff, UIKit/AppKit VideoView sample-buffer rendering, and opt-in iOS voice-chat audio-session lifecycle configuration; real-device video display validation, LiveKit E2E media validation, route/interruption audio recovery, and production runtime pacing remain incomplete.",
3434
"Production H.264 now uses real VideoToolbox encode output for publish smoke coverage, opt-in VideoToolbox decode output for subscribe smoke coverage, and UIKit/AppKit VideoView rendering for decoded frames, but production readiness still requires real-device display validation, hardware acceleration verification where the OS exposes that signal, and explicit fallback behavior instead of relying on a pure Swift codec implementation.",
3535
"SCTP data channel support includes packet-level DCEP/DataPacket planning, manager-assigned stream routing, queued local publish flushing, inbound DataPacket-to-RoomEvent plumbing, publisher SDP data-channel negotiation, subscriber-side data-channel receive-loop plumbing, OpenSSL DTLS application-data transport coverage, default Room shared-media DTLS/SRTP demux binding, deterministic packet fragmentation/reassembly envelopes, fragmented-packet retransmission scheduling on the DTLS-backed packet transport, opt-in standards-shaped SCTP association bootstrap with DATA/SACK exchange, fragmented DATA reassembly, duplicate DATA TSN suppression, contiguous cumulative SACK tracking with gap/duplicate reporting, ordered contiguous TSN delivery, outbound DATA TSN acknowledgement tracking with SACK gap-triggered retransmission of unacknowledged outbound DATA TSN holes, recovery reset that reopens LiveKit data channels after association restart, and default-gated two-client LiveKit DataPacket publish/receive smoke over that standards-shaped association transport, but full standards-compliant SCTP association state, congestion control, LiveKit-validated data-channel recovery, and E2E hardening are incomplete.",

Sources/LiveKitNative/Core/Room.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct RoomMediaStartupConfiguration: Sendable {
7070
iceRole: ICEAgentRole = .controlling,
7171
tieBreaker: UInt64 = UInt64.random(in: 1 ... UInt64.max),
7272
nominationPolicy: ICEPairNominationPolicy = .nominateFirstSuccessful,
73+
iceTransportPolicy: ICETransportPolicy = .all,
7374
localCredentials: @escaping @Sendable () -> ICECredentials? = { nil },
7475
handshaker: any DTLSSRTPHandshaking,
7576
consentFreshnessPolicy: ICEConsentFreshnessPolicy = .standard,
@@ -87,7 +88,12 @@ struct RoomMediaStartupConfiguration: Sendable {
8788
let turnRelayCandidates = turnRelayContexts.map {
8889
LocalICEUDPSocketCandidate(candidate: $0.candidate, socket: $0.socket)
8990
}
90-
let allCandidates = localCandidateSockets + serverReflexiveCandidates + turnRelayCandidates
91+
let allCandidates = switch iceTransportPolicy {
92+
case .all:
93+
localCandidateSockets + serverReflexiveCandidates + turnRelayCandidates
94+
case .relay:
95+
turnRelayCandidates
96+
}
9197
candidateStore.replace(with: allCandidates, turnRelayContexts: turnRelayContexts)
9298
return allCandidates.map(\.candidate)
9399
},
@@ -121,6 +127,7 @@ struct RoomMediaStartupConfiguration: Sendable {
121127
iceRole: ICEAgentRole = .controlling,
122128
tieBreaker: UInt64 = UInt64.random(in: 1 ... UInt64.max),
123129
nominationPolicy: ICEPairNominationPolicy = .nominateFirstSuccessful,
130+
iceTransportPolicy: ICETransportPolicy = .all,
124131
localCredentials: @escaping @Sendable () -> ICECredentials? = { nil },
125132
handshaker: any DTLSSRTPHandshaking,
126133
consentFreshnessPolicy: ICEConsentFreshnessPolicy = .standard,
@@ -135,6 +142,7 @@ struct RoomMediaStartupConfiguration: Sendable {
135142
iceRole: iceRole,
136143
tieBreaker: tieBreaker,
137144
nominationPolicy: nominationPolicy,
145+
iceTransportPolicy: iceTransportPolicy,
138146
localCredentials: localCredentials,
139147
handshaker: handshaker,
140148
consentFreshnessPolicy: consentFreshnessPolicy,
@@ -151,6 +159,7 @@ struct RoomMediaStartupConfiguration: Sendable {
151159
iceRole: ICEAgentRole = .controlling,
152160
tieBreaker: UInt64 = UInt64.random(in: 1 ... UInt64.max),
153161
nominationPolicy: ICEPairNominationPolicy = .nominateFirstSuccessful,
162+
iceTransportPolicy: ICETransportPolicy = .all,
154163
localCredentials: @escaping @Sendable () -> ICECredentials? = { nil },
155164
handshaker: any DTLSSRTPHandshaking = OpenSSLDTLSSRTPHandshaker(),
156165
consentFreshnessPolicy: ICEConsentFreshnessPolicy = .standard,
@@ -161,7 +170,8 @@ struct RoomMediaStartupConfiguration: Sendable {
161170
candidateStore: candidateStore,
162171
hostCandidateAddresses: hostCandidateAddresses,
163172
bindAddress: bindAddress,
164-
receiveTimeoutMilliseconds: receiveTimeoutMilliseconds
173+
receiveTimeoutMilliseconds: receiveTimeoutMilliseconds,
174+
iceTransportPolicy: iceTransportPolicy
165175
)
166176

167177
return Self(
@@ -200,6 +210,7 @@ struct RoomMediaStartupConfiguration: Sendable {
200210
iceRole: ICEAgentRole = .controlling,
201211
tieBreaker: UInt64 = UInt64.random(in: 1 ... UInt64.max),
202212
nominationPolicy: ICEPairNominationPolicy = .nominateFirstSuccessful,
213+
iceTransportPolicy: ICETransportPolicy = .all,
203214
localCredentials: @escaping @Sendable () -> ICECredentials? = { nil },
204215
identity: DTLSSRTPIdentity = .generated(),
205216
receiveAttemptLimit: Int = 64,
@@ -213,7 +224,8 @@ struct RoomMediaStartupConfiguration: Sendable {
213224
candidateStore: candidateStore,
214225
hostCandidateAddresses: hostCandidateAddresses,
215226
bindAddress: bindAddress,
216-
receiveTimeoutMilliseconds: receiveTimeoutMilliseconds
227+
receiveTimeoutMilliseconds: receiveTimeoutMilliseconds,
228+
iceTransportPolicy: iceTransportPolicy
217229
)
218230
let datagramFactory = LocalICEUDPSocketMediaDatagramTransportFactory(
219231
candidateStore: candidateStore,
@@ -259,19 +271,22 @@ private final class DefaultRoomMediaStartupLocalCandidateProvider: @unchecked Se
259271
private let hostCandidateAddresses: @Sendable () -> [ICEInterfaceAddress]
260272
private let bindAddress: String
261273
private let receiveTimeoutMilliseconds: Int
274+
private let iceTransportPolicy: ICETransportPolicy
262275
private let lock = NSLock()
263276
private var hostCandidates: [LocalICEUDPSocketCandidate]?
264277

265278
init(
266279
candidateStore: LocalICEUDPSocketCandidateStore,
267280
hostCandidateAddresses: @escaping @Sendable () -> [ICEInterfaceAddress],
268281
bindAddress: String,
269-
receiveTimeoutMilliseconds: Int
282+
receiveTimeoutMilliseconds: Int,
283+
iceTransportPolicy: ICETransportPolicy = .all
270284
) {
271285
self.candidateStore = candidateStore
272286
self.hostCandidateAddresses = hostCandidateAddresses
273287
self.bindAddress = bindAddress
274288
self.receiveTimeoutMilliseconds = receiveTimeoutMilliseconds
289+
self.iceTransportPolicy = iceTransportPolicy
275290
}
276291

277292
func localCandidates(iceServers: [ICEServer]) -> [ICECandidate] {
@@ -286,7 +301,12 @@ private final class DefaultRoomMediaStartupLocalCandidateProvider: @unchecked Se
286301
let turnRelayCandidates = turnRelayContexts.map {
287302
LocalICEUDPSocketCandidate(candidate: $0.candidate, socket: $0.socket)
288303
}
289-
let candidates = hostCandidates + serverReflexiveCandidates + turnRelayCandidates
304+
let candidates = switch iceTransportPolicy {
305+
case .all:
306+
hostCandidates + serverReflexiveCandidates + turnRelayCandidates
307+
case .relay:
308+
turnRelayCandidates
309+
}
290310
candidateStore.replace(with: candidates, turnRelayContexts: turnRelayContexts)
291311
return candidates.map(\.candidate)
292312
}
@@ -732,12 +752,14 @@ public final class Room: @unchecked Sendable {
732752
subscriberPeerConnection: subscriberPeerConnection,
733753
publisherPeerConnection: publisherPeerConnection,
734754
subscriberMediaStartupConfiguration: .defaultLiveMediaData(
755+
iceTransportPolicy: options.iceTransportPolicy,
735756
localCredentials: {
736757
subscriberPeerConnection.configuration.iceCredentials
737758
},
738759
identity: subscriberDTLSIdentity
739760
),
740761
publisherMediaStartupConfiguration: .defaultLiveMediaData(
762+
iceTransportPolicy: options.iceTransportPolicy,
741763
localCredentials: {
742764
publisherPeerConnection.configuration.iceCredentials
743765
},

0 commit comments

Comments
 (0)