mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
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:
parent
af173ee5c4
commit
1eb1e52912
7 changed files with 72 additions and 30 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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 = {},
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(")")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)"],
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue