session-ios/SessionMessagingKit/Calls/WebRTCSession.swift

472 lines
20 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
2021-08-10 06:19:01 +02:00
import WebRTC
import SessionUtilitiesKit
import SessionSnodeKit
2021-08-10 06:19:01 +02:00
public protocol WebRTCSessionDelegate: AnyObject {
2021-08-13 05:47:22 +02:00
var videoCapturer: RTCVideoCapturer { get }
2021-09-29 03:17:48 +02:00
func webRTCIsConnected()
func isRemoteVideoDidChange(isEnabled: Bool)
2021-10-26 06:48:31 +02:00
func dataChannelDidOpen()
2021-11-10 04:31:02 +01:00
func didReceiveHangUpSignal()
func reconnectIfNeeded()
2021-08-12 05:49:10 +02:00
}
/// See https://webrtc.org/getting-started/overview for more information.
2021-08-18 01:00:55 +02:00
public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
public weak var delegate: WebRTCSessionDelegate?
2021-10-06 08:00:12 +02:00
public let uuid: String
private let contactSessionId: String
2021-08-17 08:02:20 +02:00
private var queuedICECandidates: [RTCIceCandidate] = []
private var iceCandidateSendTimer: Timer?
2021-11-19 00:39:10 +01:00
private lazy var defaultICEServer: TurnServerInfo? = {
let url = Bundle.main.url(forResource: "Session-Turn-Server", withExtension: nil)!
let data = try! Data(contentsOf: url)
let json = try! JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as! JSON
return TurnServerInfo(attributes: json, random: 2)
2021-11-19 00:39:10 +01:00
}()
2021-08-12 02:52:41 +02:00
internal lazy var factory: RTCPeerConnectionFactory = {
2021-08-10 06:19:01 +02:00
RTCInitializeSSL()
2021-11-12 03:32:27 +01:00
let videoEncoderFactory = RTCDefaultVideoEncoderFactory()
let videoDecoderFactory = RTCDefaultVideoDecoderFactory()
return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory)
2021-08-10 06:19:01 +02:00
}()
/// Represents a WebRTC connection between the user and a remote peer. Provides methods to connect to a
/// remote peer, maintain and monitor the connection, and close the connection once it's no longer needed.
internal lazy var peerConnection: RTCPeerConnection? = {
2021-08-10 06:19:01 +02:00
let configuration = RTCConfiguration()
2021-11-19 00:39:10 +01:00
if let defaultICEServer = defaultICEServer {
configuration.iceServers = [ RTCIceServer(urlStrings: defaultICEServer.urls, username: defaultICEServer.username, credential: defaultICEServer.password) ]
2021-11-19 00:39:10 +01:00
}
2021-08-13 06:40:42 +02:00
configuration.sdpSemantics = .unifiedPlan
let constraints = RTCMediaConstraints(mandatoryConstraints: [:], optionalConstraints: [:])
2021-08-10 06:19:01 +02:00
return factory.peerConnection(with: configuration, constraints: constraints, delegate: self)
}()
// Audio
2021-08-12 02:52:41 +02:00
internal lazy var audioSource: RTCAudioSource = {
2021-08-10 06:19:01 +02:00
let constraints = RTCMediaConstraints(mandatoryConstraints: [:], optionalConstraints: [:])
return factory.audioSource(with: constraints)
}()
2021-08-12 02:52:41 +02:00
internal lazy var audioTrack: RTCAudioTrack = {
2021-08-10 06:19:01 +02:00
return factory.audioTrack(with: audioSource, trackId: "ARDAMSa0")
}()
// Video
2021-08-13 05:47:22 +02:00
public lazy var localVideoSource: RTCVideoSource = {
2021-08-18 06:16:49 +02:00
let result = factory.videoSource()
result.adaptOutputFormat(toWidth: 360, height: 780, fps: 30)
return result
2021-08-10 06:19:01 +02:00
}()
2021-08-12 02:52:41 +02:00
internal lazy var localVideoTrack: RTCVideoTrack = {
2021-08-10 06:19:01 +02:00
return factory.videoTrack(with: localVideoSource, trackId: "ARDAMSv0")
}()
2021-08-12 02:52:41 +02:00
internal lazy var remoteVideoTrack: RTCVideoTrack? = {
return peerConnection?.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack
2021-08-10 06:19:01 +02:00
}()
2021-09-27 07:08:01 +02:00
// Data Channel
2021-11-12 03:32:27 +01:00
internal var dataChannel: RTCDataChannel?
2021-09-27 07:08:01 +02:00
// MARK: - Error
public enum WebRTCSessionError: LocalizedError {
2021-08-10 06:19:01 +02:00
case noThread
public var errorDescription: String? {
switch self {
case .noThread: return "Couldn't find thread for contact."
2021-08-10 06:19:01 +02:00
}
}
}
// MARK: Initialization
2021-08-18 01:00:55 +02:00
public static var current: WebRTCSession?
public init(for contactSessionId: String, with uuid: String) {
2021-11-10 05:30:52 +01:00
RTCAudioSession.sharedInstance().useManualAudio = true
RTCAudioSession.sharedInstance().isAudioEnabled = false
self.contactSessionId = contactSessionId
2021-10-05 04:41:39 +02:00
self.uuid = uuid
2021-08-10 06:19:01 +02:00
super.init()
2021-08-13 06:40:42 +02:00
let mediaStreamTrackIDS = ["ARDAMS"]
peerConnection?.add(audioTrack, streamIds: mediaStreamTrackIDS)
peerConnection?.add(localVideoTrack, streamIds: mediaStreamTrackIDS)
2021-08-10 06:19:01 +02:00
// Configure audio session
2021-09-09 01:21:13 +02:00
configureAudioSession()
// Data channel
if let dataChannel = createDataChannel() {
dataChannel.delegate = self
2021-11-12 03:32:27 +01:00
self.dataChannel = dataChannel
}
2021-08-10 06:19:01 +02:00
}
// MARK: - Signaling
public func sendPreOffer(
_ db: Database,
message: CallMessage,
interactionId: Int64?,
in thread: SessionThread
) throws -> AnyPublisher<Void, Error> {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Sending pre-offer message.")
return MessageSender
.sendImmediate(
preparedSendData: try MessageSender
.preparedSendData(
db,
message: message,
to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant),
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: interactionId
)
)
.handleEvents(receiveOutput: { _ in SNLog("[Calls] Pre-offer message has been sent.") })
.eraseToAnyPublisher()
2021-10-05 04:41:39 +02:00
}
public func sendOffer(
_ db: Database,
to sessionId: String,
isRestartingICEConnection: Bool = false
) -> AnyPublisher<Void, Error> {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Sending offer message.")
let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(isRestartingICEConnection)
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
return Fail(error: WebRTCSessionError.noThread)
.eraseToAnyPublisher()
2021-08-10 06:19:01 +02:00
}
return Deferred {
Future<Void, Error> { [weak self] resolver in
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
2021-08-10 06:19:01 +02:00
if let error = error {
return
2021-08-10 06:19:01 +02:00
}
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
preconditionFailure()
2021-08-17 08:20:08 +02:00
}
self?.peerConnection?.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't initiate call due to error: \(error).")
resolver(Result.failure(error))
return
}
}
Storage.shared
.writePublisher { db in
try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: uuid,
kind: .offer,
sdps: [ sdp.sdp ],
sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs())
),
to: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant),
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: resolver(Result.success(()))
case .failure(let error): resolver(Result.failure(error))
}
}
)
}
2021-08-10 06:19:01 +02:00
}
}
.eraseToAnyPublisher()
}
public func sendAnswer(to sessionId: String) -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending answer message.")
let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
return Storage.shared
.readPublisher { db -> SessionThread in
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
throw WebRTCSessionError.noThread
}
return thread
}
.flatMap { [weak self] thread in
Future<Void, Error> { resolver in
self?.peerConnection?.answer(for: mediaConstraints) { [weak self] sdp, error in
if let error = error {
resolver(Result.failure(error))
return
}
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
preconditionFailure()
}
self?.peerConnection?.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't accept call due to error: \(error).")
return resolver(Result.failure(error))
}
}
Storage.shared
.writePublisher { db in
try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: uuid,
kind: .answer,
sdps: [ sdp.sdp ]
),
to: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant),
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: resolver(Result.success(()))
case .failure(let error): resolver(Result.failure(error))
}
}
)
}
}
}
.eraseToAnyPublisher()
2021-08-10 06:19:01 +02:00
}
2021-08-17 08:02:20 +02:00
private func queueICECandidateForSending(_ candidate: RTCIceCandidate) {
queuedICECandidates.append(candidate)
2021-08-17 08:20:08 +02:00
DispatchQueue.main.async {
self.iceCandidateSendTimer?.invalidate()
self.iceCandidateSendTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
self.sendICECandidates()
}
2021-08-17 08:02:20 +02:00
}
}
private func sendICECandidates() {
let candidates: [RTCIceCandidate] = self.queuedICECandidates
let uuid: String = self.uuid
let contactSessionId: String = self.contactSessionId
// Empty the queue
self.queuedICECandidates.removeAll()
Storage.shared
.writePublisher { db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else {
throw WebRTCSessionError.noThread
}
SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.")
return try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: uuid,
kind: .iceCandidates(
sdpMLineIndexes: candidates.map { UInt32($0.sdpMLineIndex) },
sdpMids: candidates.map { $0.sdpMid! }
),
sdps: candidates.map { $0.sdp }
),
to: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant),
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete()
2021-08-17 08:02:20 +02:00
}
public func endCall(_ db: Database, with sessionId: String) throws {
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: sessionId) else { return }
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Sending end call message.")
let preparedSendData: MessageSender.PreparedSendData = try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: self.uuid,
kind: .endCall,
sdps: []
),
to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant),
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
)
MessageSender
.sendImmediate(preparedSendData: preparedSendData)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()
2021-08-18 02:33:33 +02:00
}
public func dropConnection() {
peerConnection?.close()
2021-08-10 06:19:01 +02:00
}
private func mediaConstraints(_ isRestartingICEConnection: Bool) -> RTCMediaConstraints {
var mandatory: [String:String] = [
kRTCMediaConstraintsOfferToReceiveAudio : kRTCMediaConstraintsValueTrue,
kRTCMediaConstraintsOfferToReceiveVideo : kRTCMediaConstraintsValueTrue,
]
if isRestartingICEConnection { mandatory[kRTCMediaConstraintsIceRestart] = kRTCMediaConstraintsValueTrue }
let optional: [String:String] = [:]
return RTCMediaConstraints(mandatoryConstraints: mandatory, optionalConstraints: optional)
}
2021-12-07 06:30:14 +01:00
private func correctSessionDescription(sdp: RTCSessionDescription?) -> RTCSessionDescription? {
guard let sdp = sdp else { return nil }
2021-12-07 07:02:53 +01:00
let cbrSdp = sdp.sdp.description.replace(regex: "(a=fmtp:111 ((?!cbr=).)*)\r?\n", with: "$1;cbr=1\r\n")
2021-12-07 06:30:14 +01:00
let finalSdp = cbrSdp.replace(regex: ".+urn:ietf:params:rtp-hdrext:ssrc-audio-level.*\r?\n", with: "")
return RTCSessionDescription(type: sdp.type, sdp: finalSdp)
}
2021-09-27 07:08:01 +02:00
// MARK: Peer connection delegate
2021-08-10 06:19:01 +02:00
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCSignalingState) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Signaling state changed to: \(state).")
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Peer connection did add stream.")
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Peer connection did remove stream.")
2021-08-10 06:19:01 +02:00
}
public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Peer connection should negotiate.")
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceConnectionState) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] ICE connection state changed to: \(state).")
2021-09-29 03:17:48 +02:00
if state == .connected {
delegate?.webRTCIsConnected()
} else if state == .disconnected {
if self.peerConnection?.signalingState == .stable {
delegate?.reconnectIfNeeded()
}
2021-09-29 03:17:48 +02:00
}
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceGatheringState) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] ICE gathering state changed to: \(state).")
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
2021-08-17 08:02:20 +02:00
queueICECandidateForSending(candidate)
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] \(candidates.count) ICE candidate(s) removed.")
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Data channel opened.")
2021-08-10 06:19:01 +02:00
}
}
2021-09-09 01:21:13 +02:00
extension WebRTCSession {
2021-11-15 02:22:31 +01:00
public func configureAudioSession(outputAudioPort: AVAudioSession.PortOverride = .none) {
2021-09-09 01:21:13 +02:00
let audioSession = RTCAudioSession.sharedInstance()
audioSession.lockForConfiguration()
do {
try audioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue)
try audioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
2021-11-15 02:22:31 +01:00
try audioSession.overrideOutputAudioPort(outputAudioPort)
2021-09-09 01:21:13 +02:00
try audioSession.setActive(true)
} catch let error {
SNLog("Couldn't set up WebRTC audio session due to error: \(error)")
}
audioSession.unlockForConfiguration()
}
2021-11-10 05:30:52 +01:00
public func audioSessionDidActivate(_ audioSession: AVAudioSession) {
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = true
configureAudioSession()
}
public func audioSessionDidDeactivate(_ audioSession: AVAudioSession) {
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = false
}
2021-09-09 01:21:13 +02:00
public func mute() {
audioTrack.isEnabled = false
}
public func unmute() {
audioTrack.isEnabled = true
}
2021-09-22 06:54:26 +02:00
public func turnOffVideo() {
localVideoTrack.isEnabled = false
sendJSON(["video": false])
2021-09-22 06:54:26 +02:00
}
2021-10-06 08:00:12 +02:00
public func turnOnVideo() {
2021-09-22 06:54:26 +02:00
localVideoTrack.isEnabled = true
sendJSON(["video": true])
2021-09-22 06:54:26 +02:00
}
2021-11-10 04:31:02 +01:00
public func hangUp() {
sendJSON(["hangup": true])
}
2021-09-09 01:21:13 +02:00
}