session-ios/SessionMessagingKit/Calls/WebRTCSession.swift

464 lines
19 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, thread: thread),
interactionId: interactionId
)
)
.handleEvents(
receiveCompletion: { result in
switch result {
case .failure: break
case .finished: 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(receiveOn: DispatchQueue.global(qos: .userInitiated)) { 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, thread: thread),
interactionId: nil
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.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
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<SessionThread, Error> in
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
throw WebRTCSessionError.noThread
}
return Just(thread)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.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(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: uuid,
kind: .answer,
sdps: [ sdp.sdp ]
),
to: try Message.Destination.from(db, thread: thread),
interactionId: nil
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.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
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { 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 Just(
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, thread: thread),
interactionId: nil
)
)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.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, thread: thread),
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
}