Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENGDESK-38176] [Feature] Add forceRelayCandidate to Force TURN Relay and Prevent Local Network Permission Request on iOS #174

Merged
merged 5 commits into from
Feb 11, 2025
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ let txConfigUserAndPassowrd = TxConfig(sipUser: sipUser,
pushDeviceToken: "DEVICE_APNS_TOKEN",
ringtone: "incoming_call.mp3",
ringBackTone: "ringback_tone.mp3",
// Force TURN relay to avoid local network access
forceRelayCandidate: true,
//You can choose the appropriate verbosity level of the SDK.
//Logs are disabled by default
logLevel: .all)
Expand All @@ -154,6 +156,8 @@ let txConfigToken = TxConfig(token: "MY_JWT_TELNYX_TOKEN",
pushDeviceToken: "DEVICE_APNS_TOKEN",
ringtone: "incoming_call.mp3",
ringBackTone: "ringback_tone.mp3",
// Force TURN relay to avoid local network access
forceRelayCandidate: true,
//You can choose the appropriate verbosity level of the SDK. Logs are disabled by default
logLevel: .all)

Expand Down
16 changes: 14 additions & 2 deletions TelnyxRTC/Telnyx/Models/TxConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ public struct TxConfig {
/// - `logLevel`: Controls console log output in Xcode when running the app in debug mode.
/// - Important: The `debug` flag is disabled by default to minimize data usage.
public internal(set) var debug: Bool = false

/// Controls whether the SDK should force TURN relay for peer connections.
/// When enabled, the SDK will only use TURN relay candidates for ICE gathering,
/// which prevents the "local network access" permission popup from appearing.
/// - Note: Enabling this may affect the quality of calls when devices are on the same local network,
/// as all media will be relayed through TURN servers.
/// - Important: This setting is disabled by default to maintain optimal call quality.
public internal(set) var forceRelayCandidate: Bool = false

// MARK: - Initializers

Expand All @@ -53,7 +61,8 @@ public struct TxConfig {
pushEnvironment: PushEnvironment? = nil,
logLevel: LogLevel = .none,
reconnectClient: Bool = true,
debug: Bool = false
debug: Bool = false,
forceRelayCandidate: Bool = false
) {
self.sipUser = sipUser
self.password = password
Expand All @@ -66,6 +75,7 @@ public struct TxConfig {
self.reconnectClient = reconnectClient
self.pushEnvironment = pushEnvironment
self.debug = debug
self.forceRelayCandidate = forceRelayCandidate
Logger.log.verboseLevel = logLevel
}

Expand All @@ -83,7 +93,8 @@ public struct TxConfig {
ringBackTone: String? = nil,
pushEnvironment: PushEnvironment? = nil,
logLevel: LogLevel = .none,
debug: Bool = false) {
debug: Bool = false,
forceRelayCandidate: Bool = false) {
self.token = token
if let pushToken = pushDeviceToken {
//Create a notification configuration if there's an available a device push notification token
Expand All @@ -93,6 +104,7 @@ public struct TxConfig {
self.ringtone = ringtone
self.pushEnvironment = pushEnvironment
self.debug = debug
self.forceRelayCandidate = forceRelayCandidate
Logger.log.verboseLevel = logLevel
}

Expand Down
8 changes: 5 additions & 3 deletions TelnyxRTC/Telnyx/TxClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,8 @@ extension TxClient {
ringtone: self.txConfig?.ringtone,
ringbackTone: self.txConfig?.ringBackTone,
iceServers: self.serverConfiguration.webRTCIceServers,
debug: self.txConfig?.debug ?? false)
debug: self.txConfig?.debug ?? false,
forceRelayCandidate: self.txConfig?.forceRelayCandidate ?? false)
call.newCall(callerName: callerName, callerNumber: callerNumber, destinationNumber: destinationNumber, clientState: clientState, customHeaders: customHeaders)

currentCallId = callId
Expand Down Expand Up @@ -627,7 +628,8 @@ extension TxClient {
ringbackTone: self.txConfig?.ringBackTone,
iceServers: self.serverConfiguration.webRTCIceServers,
isAttach: isAttach,
debug: self.txConfig?.debug ?? false)
debug: self.txConfig?.debug ?? false,
forceRelayCandidate: self.txConfig?.forceRelayCandidate ?? false)
call.callInfo?.callerName = callerName
call.callInfo?.callerNumber = callerNumber
call.callOptions = TxCallOptions(audio: true)
Expand Down Expand Up @@ -714,7 +716,7 @@ extension TxClient {

// Create an initial call_object to handle early bye message
if let newCallId = (pushMetaData["call_id"] as? String) {
self.calls[UUID(uuidString: newCallId)!] = Call(callId: UUID(uuidString: newCallId)! , sessionId: newCallId, socket: self.socket!, delegate: self, iceServers: self.serverConfiguration.webRTCIceServers, debug: self.txConfig?.debug ?? false)
self.calls[UUID(uuidString: newCallId)!] = Call(callId: UUID(uuidString: newCallId)! , sessionId: newCallId, socket: self.socket!, delegate: self, iceServers: self.serverConfiguration.webRTCIceServers, debug: self.txConfig?.debug ?? false, forceRelayCandidate: self.txConfig?.forceRelayCandidate ?? false)
}
} catch let error {
Logger.log.e(message: "TxClient:: push flow connect error \(error.localizedDescription)")
Expand Down
23 changes: 17 additions & 6 deletions TelnyxRTC/Telnyx/WebRTC/Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ public class Call {
/// When true, the SDK will collect and send WebRTC statistics to Telnyx servers.
/// This is useful for troubleshooting call quality issues.
public internal(set) var debug: Bool = false

/// Controls whether the SDK should force TURN relay for peer connections.
/// When enabled, the SDK will only use TURN relay candidates for ICE gathering,
/// which prevents the "local network access" permission popup from appearing.
public internal(set) var forceRelayCandidate: Bool = false


// MARK: - Properties
Expand Down Expand Up @@ -192,7 +197,8 @@ public class Call {
ringbackTone: String? = nil,
iceServers: [RTCIceServer],
isAttach: Bool = false,
debug: Bool = false
debug: Bool = false,
forceRelayCandidate: Bool = false
) {
if isAttach {
self.direction = CallDirection.ATTACH
Expand Down Expand Up @@ -227,6 +233,7 @@ public class Call {
}

self.debug = debug
self.forceRelayCandidate = forceRelayCandidate
}

//Contructor for attachCalls
Expand All @@ -238,7 +245,8 @@ public class Call {
telnyxSessionId: UUID? = nil,
telnyxLegId: UUID? = nil,
iceServers: [RTCIceServer],
debug: Bool = false) {
debug: Bool = false,
forceRelayCandidate: Bool = false) {
self.direction = CallDirection.ATTACH
//Session obtained after login with the signaling socket
self.sessionId = sessionId
Expand All @@ -256,6 +264,7 @@ public class Call {
self.iceServers = iceServers

self.debug = debug
self.forceRelayCandidate = forceRelayCandidate
}

/// Constructor for outgoing calls
Expand All @@ -266,7 +275,8 @@ public class Call {
ringtone: String? = nil,
ringbackTone: String? = nil,
iceServers: [RTCIceServer],
debug: Bool = false) {
debug: Bool = false,
forceRelayCandidate: Bool = false) {
//Session obtained after login with the signaling socket
self.sessionId = sessionId
//this is the signaling server socket
Expand All @@ -283,6 +293,7 @@ public class Call {

self.updateCallState(callState: .RINGING)
self.debug = debug
self.forceRelayCandidate = forceRelayCandidate
}

// MARK: - Private functions
Expand All @@ -302,7 +313,7 @@ public class Call {
// - Create the reporter to send the startReporting message before creating the peer connection
// - Start the reporter once the peer connection is created
self.configureStatsReporter()
self.peer = Peer(iceServers: self.iceServers)
self.peer = Peer(iceServers: self.iceServers, forceRelayCandidate: self.forceRelayCandidate)
self.startStatsReporter()
self.peer?.delegate = self
self.peer?.socket = self.socket
Expand Down Expand Up @@ -431,7 +442,7 @@ extension Call {
}
self.answerCustomHeaders = customHeaders
self.configureStatsReporter()
self.peer = Peer(iceServers: self.iceServers)
self.peer = Peer(iceServers: self.iceServers, forceRelayCandidate: self.forceRelayCandidate)
self.startStatsReporter()
self.peer?.delegate = self
self.peer?.socket = self.socket
Expand Down Expand Up @@ -467,7 +478,7 @@ extension Call {
self.statsReporter?.dispose()
self.answerCustomHeaders = customHeaders
self.configureStatsReporter()
self.peer = Peer(iceServers: self.iceServers, isAttach: true)
self.peer = Peer(iceServers: self.iceServers, isAttach: true, forceRelayCandidate: self.forceRelayCandidate)
self.startStatsReporter()
self.peer?.delegate = self
self.peer?.socket = self.socket
Expand Down
9 changes: 7 additions & 2 deletions TelnyxRTC/Telnyx/WebRTC/Peer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,19 @@ class Peer : NSObject, WebRTCEventHandler {
}

required init(iceServers: [RTCIceServer],
isAttach: Bool = false) {
isAttach: Bool = false,
forceRelayCandidate: Bool = false) {
let config = RTCConfiguration()
config.iceServers = iceServers

// Unified plan is more superior than planB
config.sdpSemantics = .unifiedPlan
config.bundlePolicy = .maxCompat


// Control local network access for ICE candidate gathering
if forceRelayCandidate {
config.iceTransportPolicy = .relay // Force TURN relay to avoid local network access
}

// gatherContinually will let WebRTC to listen to any network changes and send any new candidates to the other client
config.continualGatheringPolicy = .gatherContinually
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@ extension AppDelegate : CXProviderDelegate {
logLevel: .all,
reconnectClient: true,
// Enable WebRTC stats debug
debug: true)
debug: true,
// Force relay candidate
forceRelayCandidate: false)

do {
try telnyxClient?.processVoIPNotification(txConfig: txConfig, serverConfiguration: serverConfig,pushMetaData: pushMetaData)
Expand Down
8 changes: 6 additions & 2 deletions TelnyxWebRTCDemo/ViewControllers/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ extension HomeViewController {
// You can choose the appropriate verbosity level of the SDK.
logLevel: .all,
// Enable webrtc stats debug
debug: true)
debug: true,
// Force relay candidate
forceRelayCandidate: false)
} else if let credential = sipCredential {
// To obtain SIP credentials, please go to https://portal.telnyx.com
txConfig = TxConfig(sipUser: credential.username,
Expand All @@ -303,7 +305,9 @@ extension HomeViewController {
logLevel: .all,
reconnectClient: true,
// Enable webrtc stats debug
debug: true)
debug: true,
// Force relay candidate.
forceRelayCandidate: false)

// Store user / password in user defaults
SipCredentialsManager.shared.addOrUpdateCredential(credential)
Expand Down
54 changes: 41 additions & 13 deletions docs-markdown/structs/TxConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,22 @@ Enables WebRTC communication statistics reporting to Telnyx servers.
- `logLevel`: Controls console log output in Xcode when running the app in debug mode.
- Important: The `debug` flag is disabled by default to minimize data usage.

### `forceRelayCandidate`

```swift
public internal(set) var forceRelayCandidate: Bool = false
```

Forces the WebRTC connection to use TURN relay candidates only.
- When enabled, all WebRTC traffic is routed through TURN servers, avoiding direct peer-to-peer connections.
- This setting is useful when:
- You need to bypass local network access restrictions
- You want to ensure consistent network behavior across different environments
- Privacy or security requirements mandate avoiding direct connections
- Important: Enabling this flag may slightly increase latency but provides more predictable connectivity.

## Methods
### `init(sipUser:password:pushDeviceToken:ringtone:ringBackTone:pushEnvironment:logLevel:reconnectClient:debug:)`
### `init(sipUser:password:pushDeviceToken:ringtone:ringBackTone:pushEnvironment:logLevel:reconnectClient:debug:forceRelayCandidate:)`

```swift
public init(sipUser: String, password: String,
Expand All @@ -81,7 +95,8 @@ public init(sipUser: String, password: String,
pushEnvironment: PushEnvironment? = nil,
logLevel: LogLevel = .none,
reconnectClient: Bool = true,
debug: Bool = false
debug: Bool = false,
forceRelayCandidate: Bool = false
)
```

Expand All @@ -92,20 +107,28 @@ Constructor for the Telnyx SDK configuration using SIP credentials.
- pushDeviceToken: (Optional) The device's push notification token, required for receiving inbound call notifications
- ringtone: (Optional) The audio file name to play for incoming calls (e.g., "my-ringtone.mp3")
- ringBackTone: (Optional) The audio file name to play while making outbound calls (e.g., "my-ringbacktone.mp3")
- pushEnvironment: (Optional) The environment for push notifications (development or production)
- logLevel: (Optional) The verbosity level for SDK logs (defaults to `.none`)
- reconnectClient: (Optional) Whether to automatically reconnect when connection is lost (defaults to `true`)
- debug: (Optional) Enable WebRTC statistics reporting (defaults to `false`)
- forceRelayCandidate: (Optional) Force WebRTC to use TURN relay candidates only (defaults to `false`)

#### Parameters

| Name | Description |
| ---- | ----------- |
| sipUser | The SIP username for authentication |
| password | The password associated with the SIP user |
| pushDeviceToken | (Optional) The device’s push notification token, required for receiving inbound call notifications |
| ringtone | (Optional) The audio file name to play for incoming calls (e.g., “my-ringtone.mp3”) |
| ringBackTone | (Optional) The audio file name to play while making outbound calls (e.g., “my-ringbacktone.mp3”) |
| pushDeviceToken | (Optional) The device's push notification token, required for receiving inbound call notifications |
| ringtone | (Optional) The audio file name to play for incoming calls (e.g., "my-ringtone.mp3") |
| ringBackTone | (Optional) The audio file name to play while making outbound calls (e.g., "my-ringbacktone.mp3") |
| pushEnvironment | (Optional) The environment for push notifications (development or production) |
| logLevel | (Optional) The verbosity level for SDK logs (defaults to `.none`) |
| reconnectClient | (Optional) Whether to automatically reconnect when connection is lost (defaults to `true`) |
| debug | (Optional) Enable WebRTC statistics reporting (defaults to `false`) |
| forceRelayCandidate | (Optional) Force WebRTC to use TURN relay candidates only (defaults to `false`) |

### `init(token:pushDeviceToken:ringtone:ringBackTone:pushEnvironment:logLevel:debug:)`
### `init(token:pushDeviceToken:ringtone:ringBackTone:pushEnvironment:logLevel:debug:forceRelayCandidate:)`

```swift
public init(token: String,
Expand All @@ -114,7 +137,8 @@ public init(token: String,
ringBackTone: String? = nil,
pushEnvironment: PushEnvironment? = nil,
logLevel: LogLevel = .none,
debug: Bool = false)
debug: Bool = false,
forceRelayCandidate: Bool = false)
```

Constructor for the Telnyx SDK configuration using JWT token authentication.
Expand All @@ -123,19 +147,23 @@ Constructor for the Telnyx SDK configuration using JWT token authentication.
- pushDeviceToken: (Optional) The device's push notification token, required for receiving inbound call notifications
- ringtone: (Optional) The audio file name to play for incoming calls (e.g., "my-ringtone.mp3")
- ringBackTone: (Optional) The audio file name to play while making outbound calls (e.g., "my-ringbacktone.mp3")
- pushEnvironment: (Optional) The environment for push notifications (development or production)
- logLevel: (Optional) The verbosity level for SDK logs (defaults to `.none`)
- serverConfiguration: (Optional) Custom configuration for signaling server and TURN/STUN servers (defaults to Telnyx Production servers)
- debug: (Optional) Enable WebRTC statistics reporting (defaults to `false`)
- forceRelayCandidate: (Optional) Force WebRTC to use TURN relay candidates only (defaults to `false`)

#### Parameters

| Name | Description |
| ---- | ----------- |
| token | JWT token generated from https://developers.telnyx.com/docs/v2/webrtc/quickstart |
| pushDeviceToken | (Optional) The device’s push notification token, required for receiving inbound call notifications |
| ringtone | (Optional) The audio file name to play for incoming calls (e.g., “my-ringtone.mp3”) |
| ringBackTone | (Optional) The audio file name to play while making outbound calls (e.g., “my-ringbacktone.mp3”) |
| pushDeviceToken | (Optional) The device's push notification token, required for receiving inbound call notifications |
| ringtone | (Optional) The audio file name to play for incoming calls (e.g., "my-ringtone.mp3") |
| ringBackTone | (Optional) The audio file name to play while making outbound calls (e.g., "my-ringbacktone.mp3") |
| pushEnvironment | (Optional) The environment for push notifications (development or production) |
| logLevel | (Optional) The verbosity level for SDK logs (defaults to `.none`) |
| serverConfiguration | (Optional) Custom configuration for signaling server and TURN/STUN servers (defaults to Telnyx Production servers) |
| debug | (Optional) Enable WebRTC statistics reporting (defaults to `false`) |
| forceRelayCandidate | (Optional) Force WebRTC to use TURN relay candidates only (defaults to `false`) |

### `validateParams()`

Expand All @@ -144,4 +172,4 @@ public func validateParams() throws
```

Validate if TxConfig parameters are valid
- Throws: Throws TxConfig parameters errors
- Throws: Throws TxConfig parameters errors
Loading