diff --git a/apps/android/app/src/main/assets/www/call.js b/apps/android/app/src/main/assets/www/call.js index 4c331cc539..c7cf4a9324 100644 --- a/apps/android/app/src/main/assets/www/call.js +++ b/apps/android/app/src/main/assets/www/call.js @@ -25,7 +25,8 @@ let activeCall; let answerTimeout = 30000; const processCommand = (function () { const defaultIceServers = [ - { urls: ["stun:stun.simplex.im:443?transport=tcp"] }, + { urls: ["stun:stun.simplex.im:443"] }, + { urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, { urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, ]; function getCallConfig(encodedInsertableStreams, iceServers, relay) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt index 4048ce6890..2e3f1a7e14 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt @@ -297,10 +297,10 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2) InfoText(call.callState.text) - val connInfo = - if (call.connectionInfo == null) "" - else " (${call.connectionInfo.text})" - InfoText(call.encryptionStatus + connInfo) + val connInfo = call.connectionInfo +// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})" + val connInfoText = if (connInfo == null) "" else " (${connInfo.text})" + InfoText(call.encryptionStatus + connInfoText) } } @@ -480,7 +480,10 @@ fun PreviewActiveCallOverlayVideo() { callState = CallState.Negotiated, localMedia = CallMediaType.Video, peerMedia = CallMediaType.Video, - connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host)) + connectionInfo = ConnectionInfo( + RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null), + RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null) + ) ), dismiss = {}, toggleAudio = {}, @@ -501,7 +504,10 @@ fun PreviewActiveCallOverlayAudio() { callState = CallState.Negotiated, localMedia = CallMediaType.Audio, peerMedia = CallMediaType.Audio, - connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host)) + connectionInfo = ConnectionInfo( + RTCIceCandidate(RTCIceCandidateType.Host, "udp", null), + RTCIceCandidate(RTCIceCandidateType.Host, "udp", null) + ) ), dismiss = {}, toggleAudio = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt index 93b3bd8a6f..ac586b80b4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt @@ -3,6 +3,7 @@ package chat.simplex.app.views.call import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.toUpperCase import chat.simplex.app.* import chat.simplex.app.model.Contact import chat.simplex.app.model.User @@ -11,6 +12,8 @@ import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.net.URI +import java.util.* +import kotlin.collections.ArrayList data class Call( val contact: Contact, @@ -106,17 +109,30 @@ sealed class WCallResponse { } @Serializable data class CallCapabilities(val encryption: Boolean) @Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) { - val text: String @Composable get() = when { - localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host -> - stringResource(R.string.call_connection_peer_to_peer) - localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay -> - stringResource(R.string.call_connection_via_relay) - else -> - "${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}" + val text: String @Composable get() { + val local = localCandidate?.candidateType + val remote = remoteCandidate?.candidateType + return when { + local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host -> + stringResource(R.string.call_connection_peer_to_peer) + local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay -> + stringResource(R.string.call_connection_via_relay) + else -> + "${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}" + } + } + + val protocolText: String get() { + val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown" + val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown" + val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown" + val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)" + return if (local == remote) localText else "$localText / $remote" } } + // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate -@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?) +@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?) // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer @Serializable data class RTCIceServer(val urls: List, val username: String? = null, val credential: String? = null) @@ -164,12 +180,13 @@ data class ConnectionState( fun parseRTCIceServer(str: String): RTCIceServer? { var s = replaceScheme(str, "stun:") s = replaceScheme(s, "turn:") + s = replaceScheme(s, "turns:") val u = runCatching { URI(s) }.getOrNull() if (u != null) { val scheme = u.scheme val host = u.host val port = u.port - if (u.path == "" && (scheme == "stun" || scheme == "turn")) { + if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) { val userInfo = u.userInfo?.split(":") val query = if (u.query == null || u.query == "") "" else "?${u.query}" return RTCIceServer( diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 7e914a866b..11fe43a3bf 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -228,8 +228,9 @@ struct ActiveCallOverlay: View { Text(call.callState.text) HStack { Text(call.encryptionStatus) - if let connInfo = call.connectionInfo?.text { - Text("(") + Text(connInfo) + Text(")") + if let connInfo = call.connectionInfo { +// Text("(") + Text(connInfo.text) + Text(", \(connInfo.protocolText))") + Text("(") + Text(connInfo.text) + Text(")") } } } diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index e74d05d643..0f64b82d59 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -362,22 +362,37 @@ struct ConnectionInfo: Codable, Equatable { var remoteCandidate: RTCIceCandidate? var text: LocalizedStringKey { - get { - if localCandidate?.candidateType == .host && remoteCandidate?.candidateType == .host { - return "peer-to-peer" - } else if localCandidate?.candidateType == .relay && remoteCandidate?.candidateType == .relay { - return "via relay" - } else { - let unknown = NSLocalizedString("unknown", comment: "connection info") - return "\(localCandidate?.candidateType?.rawValue ?? unknown) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)" - } + let local = localCandidate?.candidateType + let remote = remoteCandidate?.candidateType + if local == .host && remote == .host { + return "peer-to-peer" + } else if local == .relay && remote == .relay { + return "via relay" + } else { + let unknown = NSLocalizedString("unknown", comment: "connection info") + return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)" } } + + var protocolText: String { + let unknown = NSLocalizedString("unknown", comment: "connection info") + let local = localCandidate?.protocol?.uppercased() ?? unknown + let localRelay = localCandidate?.relayProtocol?.uppercased() ?? unknown + let remote = remoteCandidate?.protocol?.uppercased() ?? unknown + let localText = localRelay == local || localCandidate?.relayProtocol == nil + ? local + : "\(local) (\(localRelay))" + return local == remote + ? localText + : "\(localText) / \(remote)" + } } // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate struct RTCIceCandidate: Codable, Equatable { var candidateType: RTCIceCandidateType? + var `protocol`: String? + var relayProtocol: String? } // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type @@ -401,11 +416,12 @@ struct RTCIceServer: Codable, Equatable { func parseRTCIceServer(_ str: String) -> RTCIceServer? { var s = replaceScheme(str, "stun:") s = replaceScheme(s, "turn:") + s = replaceScheme(s, "turns:") if let u: URL = URL(string: s), let scheme = u.scheme, let host = u.host, let port = u.port, - u.path == "" && (scheme == "stun" || scheme == "turn") { + u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns") { let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "") return RTCIceServer( urls: ["\(scheme):\(host):\(port)\(query)"], diff --git a/packages/simplex-chat-webrtc/package.json b/packages/simplex-chat-webrtc/package.json index 69f6a4a6bd..8a9c9239ff 100644 --- a/packages/simplex-chat-webrtc/package.json +++ b/packages/simplex-chat-webrtc/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/webrtc", - "version": "0.2.1", + "version": "0.2.3", "description": "WebRTC call in browser and webview for SimpleX Chat clients", "main": "dist/call.js", "types": "dist/call.d.ts", diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index 773280ec79..7b0b51ea6d 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -218,7 +218,8 @@ const processCommand = (function () { } const defaultIceServers: RTCIceServer[] = [ - {urls: ["stun:stun.simplex.im:443?transport=tcp"]}, + {urls: ["stun:stun.simplex.im:443"]}, + {urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, {urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, ]