2022-05-04 23:07:26 +01:00
|
|
|
//
|
|
|
|
// WebRTC.swift
|
|
|
|
// SimpleX (iOS)
|
|
|
|
//
|
|
|
|
// Created by Evgeny on 03/05/2022.
|
|
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
2022-05-07 06:40:46 +01:00
|
|
|
import SwiftUI
|
2022-05-31 07:55:13 +01:00
|
|
|
import SimpleXChat
|
2023-03-02 16:17:01 +03:00
|
|
|
import WebRTC
|
2022-05-07 06:40:46 +01:00
|
|
|
|
2022-05-24 19:34:27 +01:00
|
|
|
class Call: ObservableObject, Equatable {
|
2022-05-07 06:40:46 +01:00
|
|
|
static func == (lhs: Call, rhs: Call) -> Bool {
|
|
|
|
lhs.contact.apiId == rhs.contact.apiId
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:34:27 +01:00
|
|
|
var direction: CallDirection
|
2022-05-07 06:40:46 +01:00
|
|
|
var contact: Contact
|
2022-05-24 19:34:27 +01:00
|
|
|
var callkitUUID: UUID?
|
2022-05-07 06:40:46 +01:00
|
|
|
var localMedia: CallMediaType
|
2022-05-24 19:34:27 +01:00
|
|
|
@Published var callState: CallState
|
|
|
|
@Published var localCapabilities: CallCapabilities?
|
|
|
|
@Published var peerMedia: CallMediaType?
|
|
|
|
@Published var sharedKey: String?
|
|
|
|
@Published var audioEnabled = true
|
2023-03-02 16:17:01 +03:00
|
|
|
@Published var speakerEnabled = false
|
2022-05-24 19:34:27 +01:00
|
|
|
@Published var videoEnabled: Bool
|
|
|
|
@Published var connectionInfo: ConnectionInfo?
|
2022-05-07 06:40:46 +01:00
|
|
|
|
|
|
|
init(
|
2022-05-24 19:34:27 +01:00
|
|
|
direction: CallDirection,
|
2022-05-07 06:40:46 +01:00
|
|
|
contact: Contact,
|
2022-05-24 19:34:27 +01:00
|
|
|
callkitUUID: UUID?,
|
2022-05-07 06:40:46 +01:00
|
|
|
callState: CallState,
|
|
|
|
localMedia: CallMediaType,
|
2022-05-24 19:34:27 +01:00
|
|
|
sharedKey: String? = nil
|
2022-05-07 06:40:46 +01:00
|
|
|
) {
|
2022-05-24 19:34:27 +01:00
|
|
|
self.direction = direction
|
2022-05-07 06:40:46 +01:00
|
|
|
self.contact = contact
|
2022-05-24 19:34:27 +01:00
|
|
|
self.callkitUUID = callkitUUID
|
2022-05-07 06:40:46 +01:00
|
|
|
self.callState = callState
|
|
|
|
self.localMedia = localMedia
|
|
|
|
self.sharedKey = sharedKey
|
2022-05-24 19:34:27 +01:00
|
|
|
self.videoEnabled = localMedia == .video
|
2022-05-07 06:40:46 +01:00
|
|
|
}
|
|
|
|
|
2022-05-19 14:33:02 +01:00
|
|
|
var encrypted: Bool { get { localEncrypted && sharedKey != nil } }
|
|
|
|
var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } }
|
|
|
|
var encryptionStatus: LocalizedStringKey {
|
|
|
|
get {
|
|
|
|
switch callState {
|
|
|
|
case .waitCapabilities: return ""
|
|
|
|
case .invitationSent: return localEncrypted ? "e2e encrypted" : "no e2e encryption"
|
2022-05-24 19:34:27 +01:00
|
|
|
case .invitationAccepted: return sharedKey == nil ? "contact has no e2e encryption" : "contact has e2e encryption"
|
2022-05-19 14:33:02 +01:00
|
|
|
default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
|
2022-05-07 06:40:46 +01:00
|
|
|
}
|
|
|
|
|
2022-05-24 19:34:27 +01:00
|
|
|
enum CallDirection {
|
|
|
|
case incoming
|
|
|
|
case outgoing
|
|
|
|
}
|
|
|
|
|
2022-05-07 06:40:46 +01:00
|
|
|
enum CallState {
|
2022-05-24 19:34:27 +01:00
|
|
|
case waitCapabilities // outgoing call started
|
|
|
|
case invitationSent // outgoing call - sent invitation
|
|
|
|
case invitationAccepted // incoming call started
|
|
|
|
case offerSent // incoming - webrtc started and offer sent
|
|
|
|
case offerReceived // outgoing - webrtc offer received via API
|
|
|
|
case answerReceived // incoming - webrtc answer received via API
|
|
|
|
case negotiated // outgoing - webrtc offer processed and answer sent, incoming - webrtc answer processed
|
2022-05-07 06:40:46 +01:00
|
|
|
case connected
|
2022-05-24 19:34:27 +01:00
|
|
|
case ended
|
2022-05-07 06:40:46 +01:00
|
|
|
|
|
|
|
var text: LocalizedStringKey {
|
|
|
|
switch self {
|
2022-05-07 13:23:20 +01:00
|
|
|
case .waitCapabilities: return "starting…"
|
|
|
|
case .invitationSent: return "waiting for answer…"
|
2022-05-24 19:34:27 +01:00
|
|
|
case .invitationAccepted: return "starting…"
|
2022-05-07 13:23:20 +01:00
|
|
|
case .offerSent: return "waiting for confirmation…"
|
|
|
|
case .offerReceived: return "received answer…"
|
2022-05-24 19:34:27 +01:00
|
|
|
case .answerReceived: return "received confirmation…"
|
2022-05-07 13:23:20 +01:00
|
|
|
case .negotiated: return "connecting…"
|
2022-05-07 06:40:46 +01:00
|
|
|
case .connected: return "connected"
|
2022-05-24 19:34:27 +01:00
|
|
|
case .ended: return "ended"
|
2022-05-07 06:40:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-05-04 23:07:26 +01:00
|
|
|
|
|
|
|
struct WVAPICall: Encodable {
|
2022-05-07 06:40:46 +01:00
|
|
|
var corrId: Int? = nil
|
2022-05-04 23:07:26 +01:00
|
|
|
var command: WCallCommand
|
|
|
|
}
|
|
|
|
|
2022-05-16 19:27:58 +01:00
|
|
|
struct WVAPIMessage: Equatable, Decodable, Encodable {
|
2022-05-04 23:07:26 +01:00
|
|
|
var corrId: Int?
|
|
|
|
var resp: WCallResponse
|
2022-05-07 06:40:46 +01:00
|
|
|
var command: WCallCommand?
|
2022-05-04 23:07:26 +01:00
|
|
|
}
|
|
|
|
|
2022-05-07 06:40:46 +01:00
|
|
|
enum WCallCommand: Equatable, Encodable, Decodable {
|
2023-03-02 16:17:01 +03:00
|
|
|
case capabilities(media: CallMediaType)
|
|
|
|
case start(media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
|
|
|
|
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
|
2022-05-16 19:27:58 +01:00
|
|
|
case answer(answer: String, iceCandidates: String)
|
|
|
|
case ice(iceCandidates: String)
|
2022-05-07 06:40:46 +01:00
|
|
|
case media(media: CallMediaType, enable: Bool)
|
2022-05-04 23:07:26 +01:00
|
|
|
case end
|
|
|
|
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
|
|
case type
|
|
|
|
case media
|
|
|
|
case aesKey
|
|
|
|
case offer
|
|
|
|
case answer
|
|
|
|
case iceCandidates
|
2022-05-07 06:40:46 +01:00
|
|
|
case enable
|
2022-05-18 17:20:43 +01:00
|
|
|
case iceServers
|
|
|
|
case relay
|
2022-05-07 06:40:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var cmdType: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case .capabilities: return "capabilities"
|
|
|
|
case .start: return "start"
|
2022-05-16 19:27:58 +01:00
|
|
|
case .offer: return "offer"
|
2022-05-07 06:40:46 +01:00
|
|
|
case .answer: return "answer"
|
|
|
|
case .ice: return "ice"
|
|
|
|
case .media: return "media"
|
|
|
|
case .end: return "end"
|
|
|
|
}
|
|
|
|
}
|
2022-05-04 23:07:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
switch self {
|
2023-03-02 16:17:01 +03:00
|
|
|
case let .capabilities(media):
|
2022-05-04 23:07:26 +01:00
|
|
|
try container.encode("capabilities", forKey: .type)
|
2022-05-27 16:36:33 +01:00
|
|
|
try container.encode(media, forKey: .media)
|
2023-03-02 16:17:01 +03:00
|
|
|
case let .start(media, aesKey, iceServers, relay):
|
2022-05-04 23:07:26 +01:00
|
|
|
try container.encode("start", forKey: .type)
|
|
|
|
try container.encode(media, forKey: .media)
|
|
|
|
try container.encode(aesKey, forKey: .aesKey)
|
2022-05-18 17:20:43 +01:00
|
|
|
try container.encode(iceServers, forKey: .iceServers)
|
|
|
|
try container.encode(relay, forKey: .relay)
|
2023-03-02 16:17:01 +03:00
|
|
|
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
|
2022-05-18 07:01:32 +01:00
|
|
|
try container.encode("offer", forKey: .type)
|
2022-05-04 23:07:26 +01:00
|
|
|
try container.encode(offer, forKey: .offer)
|
|
|
|
try container.encode(iceCandidates, forKey: .iceCandidates)
|
|
|
|
try container.encode(media, forKey: .media)
|
|
|
|
try container.encode(aesKey, forKey: .aesKey)
|
2022-05-18 17:20:43 +01:00
|
|
|
try container.encode(iceServers, forKey: .iceServers)
|
|
|
|
try container.encode(relay, forKey: .relay)
|
2022-05-04 23:07:26 +01:00
|
|
|
case let .answer(answer, iceCandidates):
|
|
|
|
try container.encode("answer", forKey: .type)
|
|
|
|
try container.encode(answer, forKey: .answer)
|
|
|
|
try container.encode(iceCandidates, forKey: .iceCandidates)
|
|
|
|
case let .ice(iceCandidates):
|
|
|
|
try container.encode("ice", forKey: .type)
|
|
|
|
try container.encode(iceCandidates, forKey: .iceCandidates)
|
2022-05-07 06:40:46 +01:00
|
|
|
case let .media(media, enable):
|
|
|
|
try container.encode("media", forKey: .type)
|
|
|
|
try container.encode(media, forKey: .media)
|
|
|
|
try container.encode(enable, forKey: .enable)
|
2022-05-04 23:07:26 +01:00
|
|
|
case .end:
|
|
|
|
try container.encode("end", forKey: .type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
init(from decoder: Decoder) throws {
|
|
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
|
|
|
switch type {
|
|
|
|
case "capabilities":
|
2022-05-27 16:36:33 +01:00
|
|
|
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
2023-03-02 16:17:01 +03:00
|
|
|
self = .capabilities(media: media)
|
2022-05-04 23:07:26 +01:00
|
|
|
case "start":
|
|
|
|
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
|
|
|
let aesKey = try? container.decode(String.self, forKey: CodingKeys.aesKey)
|
2022-05-18 17:20:43 +01:00
|
|
|
let iceServers = try container.decode(([RTCIceServer]?).self, forKey: .iceServers)
|
|
|
|
let relay = try container.decode((Bool?).self, forKey: .relay)
|
2023-03-02 16:17:01 +03:00
|
|
|
self = .start(media: media, aesKey: aesKey, iceServers: iceServers, relay: relay)
|
2022-05-16 19:27:58 +01:00
|
|
|
case "offer":
|
2022-05-04 23:07:26 +01:00
|
|
|
let offer = try container.decode(String.self, forKey: CodingKeys.offer)
|
2022-05-16 19:27:58 +01:00
|
|
|
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
2022-05-04 23:07:26 +01:00
|
|
|
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
|
|
|
let aesKey = try? container.decode(String.self, forKey: CodingKeys.aesKey)
|
2022-05-18 17:20:43 +01:00
|
|
|
let iceServers = try container.decode(([RTCIceServer]?).self, forKey: .iceServers)
|
|
|
|
let relay = try container.decode((Bool?).self, forKey: .relay)
|
2023-03-02 16:17:01 +03:00
|
|
|
self = .offer(offer: offer, iceCandidates: iceCandidates, media: media, aesKey: aesKey, iceServers: iceServers, relay: relay)
|
2022-05-04 23:07:26 +01:00
|
|
|
case "answer":
|
|
|
|
let answer = try container.decode(String.self, forKey: CodingKeys.answer)
|
2022-05-16 19:27:58 +01:00
|
|
|
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
2022-05-04 23:07:26 +01:00
|
|
|
self = .answer(answer: answer, iceCandidates: iceCandidates)
|
|
|
|
case "ice":
|
2022-05-16 19:27:58 +01:00
|
|
|
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
2022-05-04 23:07:26 +01:00
|
|
|
self = .ice(iceCandidates: iceCandidates)
|
2022-05-07 06:40:46 +01:00
|
|
|
case "media":
|
|
|
|
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
|
|
|
let enable = try container.decode(Bool.self, forKey: CodingKeys.enable)
|
|
|
|
self = .media(media: media, enable: enable)
|
2022-05-04 23:07:26 +01:00
|
|
|
case "end":
|
|
|
|
self = .end
|
|
|
|
default:
|
|
|
|
throw DecodingError.typeMismatch(WCallCommand.self, DecodingError.Context(codingPath: [CodingKeys.type], debugDescription: "cannot decode WCallCommand, unknown type \(type)"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-05-07 06:40:46 +01:00
|
|
|
enum WCallResponse: Equatable, Decodable {
|
2022-05-04 23:07:26 +01:00
|
|
|
case capabilities(capabilities: CallCapabilities)
|
2022-05-16 19:27:58 +01:00
|
|
|
case offer(offer: String, iceCandidates: String, capabilities: CallCapabilities)
|
|
|
|
case answer(answer: String, iceCandidates: String)
|
|
|
|
case ice(iceCandidates: String)
|
2022-05-04 23:07:26 +01:00
|
|
|
case connection(state: ConnectionState)
|
2022-05-18 17:20:43 +01:00
|
|
|
case connected(connectionInfo: ConnectionInfo)
|
2022-05-04 23:07:26 +01:00
|
|
|
case ended
|
|
|
|
case ok
|
|
|
|
case error(message: String)
|
|
|
|
case invalid(type: String)
|
|
|
|
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
|
|
case type
|
|
|
|
case capabilities
|
|
|
|
case offer
|
|
|
|
case answer
|
|
|
|
case iceCandidates
|
|
|
|
case state
|
2022-05-18 17:20:43 +01:00
|
|
|
case connectionInfo
|
2022-05-04 23:07:26 +01:00
|
|
|
case message
|
|
|
|
}
|
|
|
|
|
2022-05-07 06:40:46 +01:00
|
|
|
var respType: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
2022-05-24 19:34:27 +01:00
|
|
|
case .capabilities: return "capabilities"
|
|
|
|
case .offer: return "offer"
|
|
|
|
case .answer: return "answer"
|
|
|
|
case .ice: return "ice"
|
|
|
|
case .connection: return "connection"
|
|
|
|
case .connected: return "connected"
|
|
|
|
case .ended: return "ended"
|
|
|
|
case .ok: return "ok"
|
|
|
|
case .error: return "error"
|
|
|
|
case .invalid: return "invalid"
|
2022-05-07 06:40:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-04 23:07:26 +01:00
|
|
|
init(from decoder: Decoder) throws {
|
|
|
|
do {
|
|
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
|
|
|
switch type {
|
|
|
|
case "capabilities":
|
|
|
|
let capabilities = try container.decode(CallCapabilities.self, forKey: CodingKeys.capabilities)
|
|
|
|
self = .capabilities(capabilities: capabilities)
|
|
|
|
case "offer":
|
|
|
|
let offer = try container.decode(String.self, forKey: CodingKeys.offer)
|
2022-05-16 19:27:58 +01:00
|
|
|
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
2022-05-07 06:40:46 +01:00
|
|
|
let capabilities = try container.decode(CallCapabilities.self, forKey: CodingKeys.capabilities)
|
|
|
|
self = .offer(offer: offer, iceCandidates: iceCandidates, capabilities: capabilities)
|
2022-05-04 23:07:26 +01:00
|
|
|
case "answer":
|
|
|
|
let answer = try container.decode(String.self, forKey: CodingKeys.answer)
|
2022-05-16 19:27:58 +01:00
|
|
|
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
2022-05-04 23:07:26 +01:00
|
|
|
self = .answer(answer: answer, iceCandidates: iceCandidates)
|
|
|
|
case "ice":
|
2022-05-16 19:27:58 +01:00
|
|
|
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
2022-05-04 23:07:26 +01:00
|
|
|
self = .ice(iceCandidates: iceCandidates)
|
|
|
|
case "connection":
|
|
|
|
let state = try container.decode(ConnectionState.self, forKey: CodingKeys.state)
|
|
|
|
self = .connection(state: state)
|
2022-05-18 17:20:43 +01:00
|
|
|
case "connected":
|
|
|
|
let connectionInfo = try container.decode(ConnectionInfo.self, forKey: CodingKeys.connectionInfo)
|
|
|
|
self = .connected(connectionInfo: connectionInfo)
|
2022-05-04 23:07:26 +01:00
|
|
|
case "ended":
|
|
|
|
self = .ended
|
|
|
|
case "ok":
|
|
|
|
self = .ok
|
|
|
|
case "error":
|
|
|
|
let message = try container.decode(String.self, forKey: CodingKeys.message)
|
|
|
|
self = .error(message: message)
|
|
|
|
default:
|
|
|
|
self = .invalid(type: type)
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
self = .invalid(type: "unknown")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-07 06:40:46 +01:00
|
|
|
// This protocol is for debugging
|
2022-05-16 19:27:58 +01:00
|
|
|
extension WCallResponse: Encodable {
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
switch self {
|
|
|
|
case .capabilities:
|
|
|
|
try container.encode("capabilities", forKey: .type)
|
|
|
|
case let .offer(offer, iceCandidates, capabilities):
|
|
|
|
try container.encode("offer", forKey: .type)
|
|
|
|
try container.encode(offer, forKey: .offer)
|
|
|
|
try container.encode(iceCandidates, forKey: .iceCandidates)
|
|
|
|
try container.encode(capabilities, forKey: .capabilities)
|
|
|
|
case let .answer(answer, iceCandidates):
|
|
|
|
try container.encode("answer", forKey: .type)
|
|
|
|
try container.encode(answer, forKey: .answer)
|
|
|
|
try container.encode(iceCandidates, forKey: .iceCandidates)
|
|
|
|
case let .ice(iceCandidates):
|
|
|
|
try container.encode("ice", forKey: .type)
|
|
|
|
try container.encode(iceCandidates, forKey: .iceCandidates)
|
|
|
|
case let .connection(state):
|
|
|
|
try container.encode("connection", forKey: .type)
|
|
|
|
try container.encode(state, forKey: .state)
|
2022-05-18 17:20:43 +01:00
|
|
|
case let .connected(connectionInfo):
|
|
|
|
try container.encode("connected", forKey: .type)
|
|
|
|
try container.encode(connectionInfo, forKey: .connectionInfo)
|
2022-05-16 19:27:58 +01:00
|
|
|
case .ended:
|
|
|
|
try container.encode("ended", forKey: .type)
|
|
|
|
case .ok:
|
|
|
|
try container.encode("ok", forKey: .type)
|
|
|
|
case let .error(message):
|
|
|
|
try container.encode("error", forKey: .type)
|
|
|
|
try container.encode(message, forKey: .message)
|
|
|
|
case let .invalid(type):
|
|
|
|
try container.encode(type, forKey: .type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-05-04 23:07:26 +01:00
|
|
|
|
2022-05-07 06:40:46 +01:00
|
|
|
struct ConnectionState: Codable, Equatable {
|
2022-05-04 23:07:26 +01:00
|
|
|
var connectionState: String
|
|
|
|
var iceConnectionState: String
|
|
|
|
var iceGatheringState: String
|
|
|
|
var signalingState: String
|
|
|
|
}
|
2022-05-18 17:20:43 +01:00
|
|
|
|
|
|
|
struct ConnectionInfo: Codable, Equatable {
|
|
|
|
var localCandidate: RTCIceCandidate?
|
|
|
|
var remoteCandidate: RTCIceCandidate?
|
2022-05-20 07:43:44 +01:00
|
|
|
|
|
|
|
var text: LocalizedStringKey {
|
2023-02-06 00:57:50 +03:00
|
|
|
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)"
|
2022-05-20 07:43:44 +01:00
|
|
|
}
|
|
|
|
}
|
2023-02-06 00:57:50 +03:00
|
|
|
|
|
|
|
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)"
|
|
|
|
}
|
2022-05-18 17:20:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
|
|
|
struct RTCIceCandidate: Codable, Equatable {
|
|
|
|
var candidateType: RTCIceCandidateType?
|
2023-02-06 00:57:50 +03:00
|
|
|
var `protocol`: String?
|
|
|
|
var relayProtocol: String?
|
2023-03-02 16:17:01 +03:00
|
|
|
var sdpMid: String?
|
|
|
|
var sdpMLineIndex: Int?
|
|
|
|
var candidate: String
|
2022-05-18 17:20:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
|
|
|
|
enum RTCIceCandidateType: String, Codable {
|
|
|
|
case host = "host"
|
|
|
|
case serverReflexive = "srflx"
|
|
|
|
case peerReflexive = "prflx"
|
|
|
|
case relay = "relay"
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
|
|
|
struct RTCIceServer: Codable, Equatable {
|
|
|
|
var urls: [String]
|
|
|
|
var username: String? = nil
|
|
|
|
var credential: String? = nil
|
|
|
|
}
|
2022-09-21 10:19:13 +01:00
|
|
|
|
|
|
|
// the servers are expected in this format:
|
2023-02-04 15:44:39 +00:00
|
|
|
// stun:stun.simplex.im:443?transport=tcp
|
|
|
|
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
|
2022-09-21 10:19:13 +01:00
|
|
|
func parseRTCIceServer(_ str: String) -> RTCIceServer? {
|
|
|
|
var s = replaceScheme(str, "stun:")
|
|
|
|
s = replaceScheme(s, "turn:")
|
2023-02-06 00:57:50 +03:00
|
|
|
s = replaceScheme(s, "turns:")
|
2022-09-21 10:19:13 +01:00
|
|
|
if let u: URL = URL(string: s),
|
|
|
|
let scheme = u.scheme,
|
|
|
|
let host = u.host,
|
|
|
|
let port = u.port,
|
2023-02-06 00:57:50 +03:00
|
|
|
u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns") {
|
2023-02-04 15:44:39 +00:00
|
|
|
let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "")
|
2022-09-21 10:19:13 +01:00
|
|
|
return RTCIceServer(
|
2023-02-04 15:44:39 +00:00
|
|
|
urls: ["\(scheme):\(host):\(port)\(query)"],
|
2022-09-21 10:19:13 +01:00
|
|
|
username: u.user,
|
|
|
|
credential: u.password
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
private func replaceScheme(_ s: String, _ scheme: String) -> String {
|
|
|
|
s.starts(with: scheme)
|
|
|
|
? s.replacingOccurrences(of: scheme, with: scheme + "//", options: .anchored, range: nil)
|
|
|
|
: s
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseRTCIceServers(_ servers: [String]) -> [RTCIceServer]? {
|
|
|
|
var iceServers: [RTCIceServer] = []
|
|
|
|
for s in servers {
|
|
|
|
if let server = parseRTCIceServer(s) {
|
|
|
|
iceServers.append(server)
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return iceServers.isEmpty ? nil : iceServers
|
|
|
|
}
|
|
|
|
|
|
|
|
func getIceServers() -> [RTCIceServer]? {
|
|
|
|
if let servers = UserDefaults.standard.stringArray(forKey: DEFAULT_WEBRTC_ICE_SERVERS) {
|
|
|
|
return parseRTCIceServers(servers)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-03-02 16:17:01 +03:00
|
|
|
|
|
|
|
func getWebRTCIceServers() -> [WebRTC.RTCIceServer]? {
|
|
|
|
if let servers = UserDefaults.standard.stringArray(forKey: DEFAULT_WEBRTC_ICE_SERVERS) {
|
|
|
|
return parseRTCIceServers(servers)?.toWebRTCIceServers()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|