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
|
|
|
|
|
|
|
|
class Call: Equatable {
|
|
|
|
static func == (lhs: Call, rhs: Call) -> Bool {
|
|
|
|
lhs.contact.apiId == rhs.contact.apiId
|
|
|
|
}
|
|
|
|
|
|
|
|
var contact: Contact
|
|
|
|
var callState: CallState
|
|
|
|
var localMedia: CallMediaType
|
|
|
|
var localCapabilities: CallCapabilities?
|
|
|
|
var peerMedia: CallMediaType?
|
|
|
|
var sharedKey: String?
|
|
|
|
var audioEnabled: Bool
|
|
|
|
var videoEnabled: Bool
|
|
|
|
|
|
|
|
init(
|
|
|
|
contact: Contact,
|
|
|
|
callState: CallState,
|
|
|
|
localMedia: CallMediaType,
|
|
|
|
localCapabilities: CallCapabilities? = nil,
|
|
|
|
peerMedia: CallMediaType? = nil,
|
|
|
|
sharedKey: String? = nil,
|
|
|
|
audioEnabled: Bool? = nil,
|
|
|
|
videoEnabled: Bool? = nil
|
|
|
|
) {
|
|
|
|
self.contact = contact
|
|
|
|
self.callState = callState
|
|
|
|
self.localMedia = localMedia
|
|
|
|
self.localCapabilities = localCapabilities
|
|
|
|
self.peerMedia = peerMedia
|
|
|
|
self.sharedKey = sharedKey
|
|
|
|
self.audioEnabled = audioEnabled ?? true
|
|
|
|
self.videoEnabled = videoEnabled ?? (localMedia == .video)
|
|
|
|
}
|
|
|
|
|
|
|
|
func copy(
|
|
|
|
contact: Contact? = nil,
|
|
|
|
callState: CallState? = nil,
|
|
|
|
localMedia: CallMediaType? = nil,
|
|
|
|
localCapabilities: CallCapabilities? = nil,
|
|
|
|
peerMedia: CallMediaType? = nil,
|
|
|
|
sharedKey: String? = nil,
|
|
|
|
audioEnabled: Bool? = nil,
|
|
|
|
videoEnabled: Bool? = nil
|
|
|
|
) -> Call {
|
|
|
|
Call (
|
|
|
|
contact: contact ?? self.contact,
|
|
|
|
callState: callState ?? self.callState,
|
|
|
|
localMedia: localMedia ?? self.localMedia,
|
|
|
|
localCapabilities: localCapabilities ?? self.localCapabilities,
|
|
|
|
peerMedia: peerMedia ?? self.peerMedia,
|
|
|
|
sharedKey: sharedKey ?? self.sharedKey,
|
|
|
|
audioEnabled: audioEnabled ?? self.audioEnabled,
|
|
|
|
videoEnabled: videoEnabled ?? self.videoEnabled
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
var encrypted: Bool {
|
|
|
|
(localCapabilities?.encryption ?? false) && sharedKey != nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum CallState {
|
|
|
|
case waitCapabilities
|
|
|
|
case invitationSent
|
|
|
|
case invitationReceived
|
|
|
|
case offerSent
|
|
|
|
case offerReceived
|
|
|
|
case negotiated
|
|
|
|
case connected
|
|
|
|
|
|
|
|
var text: LocalizedStringKey {
|
|
|
|
switch self {
|
2022-05-07 13:23:20 +01:00
|
|
|
case .waitCapabilities: return "starting…"
|
|
|
|
case .invitationSent: return "waiting for answer…"
|
|
|
|
case .invitationReceived: return "starting…"
|
|
|
|
case .offerSent: return "waiting for confirmation…"
|
|
|
|
case .offerReceived: return "received answer…"
|
|
|
|
case .negotiated: return "connecting…"
|
2022-05-07 06:40:46 +01:00
|
|
|
case .connected: return "connected"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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 {
|
2022-05-16 19:27:58 +01:00
|
|
|
case capabilities(useWorker: Bool? = nil)
|
|
|
|
case start(media: CallMediaType, aesKey: String? = nil, useWorker: Bool? = nil)
|
|
|
|
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, useWorker: Bool? = nil)
|
|
|
|
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
|
2022-05-16 19:27:58 +01:00
|
|
|
case useWorker
|
2022-05-04 23:07:26 +01:00
|
|
|
case offer
|
|
|
|
case answer
|
|
|
|
case iceCandidates
|
2022-05-07 06:40:46 +01:00
|
|
|
case enable
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2022-05-16 19:27:58 +01:00
|
|
|
case let .capabilities(useWorker):
|
2022-05-04 23:07:26 +01:00
|
|
|
try container.encode("capabilities", forKey: .type)
|
2022-05-16 19:27:58 +01:00
|
|
|
try container.encode(useWorker, forKey: .useWorker)
|
|
|
|
case let .start(media, aesKey, useWorker):
|
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-16 19:27:58 +01:00
|
|
|
try container.encode(useWorker, forKey: .useWorker)
|
|
|
|
case let .offer(offer, iceCandidates, media, aesKey, useWorker):
|
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-16 19:27:58 +01:00
|
|
|
try container.encode(useWorker, forKey: .useWorker)
|
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-16 19:27:58 +01:00
|
|
|
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
|
|
|
|
self = .capabilities(useWorker: useWorker)
|
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-16 19:27:58 +01:00
|
|
|
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
|
|
|
|
self = .start(media: media, aesKey: aesKey, useWorker: useWorker)
|
|
|
|
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-16 19:27:58 +01:00
|
|
|
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
|
|
|
|
self = .offer(offer: offer, iceCandidates: iceCandidates, media: media, aesKey: aesKey, useWorker: useWorker)
|
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)
|
|
|
|
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
|
|
|
|
case message
|
|
|
|
// TODO remove media, aesKey
|
|
|
|
case media
|
|
|
|
case aesKey
|
|
|
|
}
|
|
|
|
|
2022-05-07 06:40:46 +01:00
|
|
|
var respType: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case .capabilities: return("capabilities")
|
|
|
|
case .offer: return("offer")
|
|
|
|
case .answer: return("answer (TODO remove)")
|
|
|
|
case .ice: return("ice")
|
|
|
|
case .connection: return("connection")
|
|
|
|
case .ended: return("ended")
|
|
|
|
case .ok: return("ok")
|
|
|
|
case .error: return("error")
|
|
|
|
case .invalid: return("invalid")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
}
|