call.ts: include udp stun/turn (#1892)

* call.ts: include udp stun/turn

* update JS

* show protocol, support TURNS

* mobile: add turn via UDP, remove protocol from view

* remove enums for protocol strings in ICE candidates

* 0.2.3

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
sh 2023-02-06 00:57:50 +03:00 committed by GitHub
parent af173ee5c4
commit 1eb1e52912
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 72 additions and 30 deletions

View file

@ -25,7 +25,8 @@ let activeCall;
let answerTimeout = 30000; let answerTimeout = 30000;
const processCommand = (function () { const processCommand = (function () {
const defaultIceServers = [ 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" }, { urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
]; ];
function getCallConfig(encodedInsertableStreams, iceServers, relay) { function getCallConfig(encodedInsertableStreams, iceServers, relay) {

View file

@ -297,10 +297,10 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2) InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
InfoText(call.callState.text) InfoText(call.callState.text)
val connInfo = val connInfo = call.connectionInfo
if (call.connectionInfo == null) "" // val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
else " (${call.connectionInfo.text})" val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
InfoText(call.encryptionStatus + connInfo) InfoText(call.encryptionStatus + connInfoText)
} }
} }
@ -480,7 +480,10 @@ fun PreviewActiveCallOverlayVideo() {
callState = CallState.Negotiated, callState = CallState.Negotiated,
localMedia = CallMediaType.Video, localMedia = CallMediaType.Video,
peerMedia = 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 = {}, dismiss = {},
toggleAudio = {}, toggleAudio = {},
@ -501,7 +504,10 @@ fun PreviewActiveCallOverlayAudio() {
callState = CallState.Negotiated, callState = CallState.Negotiated,
localMedia = CallMediaType.Audio, localMedia = CallMediaType.Audio,
peerMedia = 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 = {}, dismiss = {},
toggleAudio = {}, toggleAudio = {},

View file

@ -3,6 +3,7 @@ package chat.simplex.app.views.call
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.toUpperCase
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.model.Contact import chat.simplex.app.model.Contact
import chat.simplex.app.model.User import chat.simplex.app.model.User
@ -11,6 +12,8 @@ import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
import java.util.*
import kotlin.collections.ArrayList
data class Call( data class Call(
val contact: Contact, val contact: Contact,
@ -106,17 +109,30 @@ sealed class WCallResponse {
} }
@Serializable data class CallCapabilities(val encryption: Boolean) @Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) { @Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
val text: String @Composable get() = when { val text: String @Composable get() {
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host -> val local = localCandidate?.candidateType
stringResource(R.string.call_connection_peer_to_peer) val remote = remoteCandidate?.candidateType
localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay -> return when {
stringResource(R.string.call_connection_via_relay) local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
else -> stringResource(R.string.call_connection_peer_to_peer)
"${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}" 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 // 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 // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null) @Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
@ -164,12 +180,13 @@ data class ConnectionState(
fun parseRTCIceServer(str: String): RTCIceServer? { fun parseRTCIceServer(str: String): RTCIceServer? {
var s = replaceScheme(str, "stun:") var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "turn:") s = replaceScheme(s, "turn:")
s = replaceScheme(s, "turns:")
val u = runCatching { URI(s) }.getOrNull() val u = runCatching { URI(s) }.getOrNull()
if (u != null) { if (u != null) {
val scheme = u.scheme val scheme = u.scheme
val host = u.host val host = u.host
val port = u.port 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 userInfo = u.userInfo?.split(":")
val query = if (u.query == null || u.query == "") "" else "?${u.query}" val query = if (u.query == null || u.query == "") "" else "?${u.query}"
return RTCIceServer( return RTCIceServer(

View file

@ -228,8 +228,9 @@ struct ActiveCallOverlay: View {
Text(call.callState.text) Text(call.callState.text)
HStack { HStack {
Text(call.encryptionStatus) Text(call.encryptionStatus)
if let connInfo = call.connectionInfo?.text { if let connInfo = call.connectionInfo {
Text("(") + Text(connInfo) + Text(")") // Text("(") + Text(connInfo.text) + Text(", \(connInfo.protocolText))")
Text("(") + Text(connInfo.text) + Text(")")
} }
} }
} }

View file

@ -362,22 +362,37 @@ struct ConnectionInfo: Codable, Equatable {
var remoteCandidate: RTCIceCandidate? var remoteCandidate: RTCIceCandidate?
var text: LocalizedStringKey { var text: LocalizedStringKey {
get { let local = localCandidate?.candidateType
if localCandidate?.candidateType == .host && remoteCandidate?.candidateType == .host { let remote = remoteCandidate?.candidateType
return "peer-to-peer" if local == .host && remote == .host {
} else if localCandidate?.candidateType == .relay && remoteCandidate?.candidateType == .relay { return "peer-to-peer"
return "via relay" } else if local == .relay && remote == .relay {
} else { return "via relay"
let unknown = NSLocalizedString("unknown", comment: "connection info") } else {
return "\(localCandidate?.candidateType?.rawValue ?? unknown) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)" 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 // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
struct RTCIceCandidate: Codable, Equatable { struct RTCIceCandidate: Codable, Equatable {
var candidateType: RTCIceCandidateType? var candidateType: RTCIceCandidateType?
var `protocol`: String?
var relayProtocol: String?
} }
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
@ -401,11 +416,12 @@ struct RTCIceServer: Codable, Equatable {
func parseRTCIceServer(_ str: String) -> RTCIceServer? { func parseRTCIceServer(_ str: String) -> RTCIceServer? {
var s = replaceScheme(str, "stun:") var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "turn:") s = replaceScheme(s, "turn:")
s = replaceScheme(s, "turns:")
if let u: URL = URL(string: s), if let u: URL = URL(string: s),
let scheme = u.scheme, let scheme = u.scheme,
let host = u.host, let host = u.host,
let port = u.port, 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 ?? "") let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "")
return RTCIceServer( return RTCIceServer(
urls: ["\(scheme):\(host):\(port)\(query)"], urls: ["\(scheme):\(host):\(port)\(query)"],

View file

@ -1,6 +1,6 @@
{ {
"name": "@simplex-chat/webrtc", "name": "@simplex-chat/webrtc",
"version": "0.2.1", "version": "0.2.3",
"description": "WebRTC call in browser and webview for SimpleX Chat clients", "description": "WebRTC call in browser and webview for SimpleX Chat clients",
"main": "dist/call.js", "main": "dist/call.js",
"types": "dist/call.d.ts", "types": "dist/call.d.ts",

View file

@ -218,7 +218,8 @@ const processCommand = (function () {
} }
const defaultIceServers: RTCIceServer[] = [ 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"}, {urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"},
] ]