session-ios/SessionMessagingKit/Calls/WebRTCSession.swift

299 lines
12 KiB
Swift
Raw Normal View History

2021-08-10 06:19:01 +02:00
import PromiseKit
import WebRTC
2021-08-18 01:00:55 +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 webRTCDidConnected()
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?
private let contactSessionID: String
2021-10-05 04:41:39 +02:00
private let uuid: String
2021-08-17 08:02:20 +02:00
private var queuedICECandidates: [RTCIceCandidate] = []
private var iceCandidateSendTimer: Timer?
private let defaultICEServers = [
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302",
"stun:stun3.l.google.com:19302",
"stun:stun4.l.google.com:19302"
]
2021-08-10 06:19:01 +02:00
2021-08-12 02:52:41 +02:00
internal lazy var factory: RTCPeerConnectionFactory = {
2021-08-10 06:19:01 +02:00
RTCInitializeSSL()
2021-08-27 08:53:08 +02:00
return RTCPeerConnectionFactory()
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.
2021-08-12 02:52:41 +02:00
internal lazy var peerConnection: RTCPeerConnection = {
2021-08-10 06:19:01 +02:00
let configuration = RTCConfiguration()
configuration.iceServers = [ RTCIceServer(urlStrings: defaultICEServers) ]
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)
}()
internal lazy var mediaConstraints: RTCMediaConstraints = {
2021-08-10 06:19:01 +02:00
let mandatory: [String:String] = [
kRTCMediaConstraintsOfferToReceiveAudio : kRTCMediaConstraintsValueTrue,
2021-08-18 06:16:49 +02:00
kRTCMediaConstraintsOfferToReceiveVideo : kRTCMediaConstraintsValueTrue,
2021-08-10 06:19:01 +02:00
]
let optional: [String:String] = [:]
return RTCMediaConstraints(mandatoryConstraints: mandatory, optionalConstraints: optional)
}()
// 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? = {
2021-08-13 06:40:42 +02:00
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
internal var dataChannel: RTCDataChannel?
2021-08-10 06:19:01 +02:00
// MARK: Error
public enum Error : LocalizedError {
case noThread
public var errorDescription: String? {
switch self {
case .noThread: return "Couldn't find thread for contact."
}
}
}
// MARK: Initialization
2021-08-18 01:00:55 +02:00
public static var current: WebRTCSession?
2021-10-05 04:41:39 +02:00
public init(for contactSessionID: String, with uuid: String) {
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"]
2021-09-27 07:08:01 +02:00
createDataChannel()
2021-08-13 06:40:42 +02:00
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()
2021-08-10 06:19:01 +02:00
}
2021-08-17 08:02:20 +02:00
// MARK: Signaling
2021-10-05 04:41:39 +02:00
public func sendPreOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
print("[Calls] Sending pre-offer message.")
guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) }
let (promise, seal) = Promise<Void>.pending()
DispatchQueue.main.async {
let message = CallMessage()
message.uuid = self.uuid
message.kind = .preOffer
MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 {
print("[Calls] Pre-offer message has been sent.")
seal.fulfill(())
}.catch2 { error in
seal.reject(error)
}
}
return promise
}
public func sendOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
2021-08-17 08:20:08 +02:00
print("[Calls] Sending offer message.")
guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) }
2021-08-10 06:19:01 +02:00
let (promise, seal) = Promise<Void>.pending()
peerConnection.offer(for: mediaConstraints) { [weak self] sdp, error in
2021-08-10 06:19:01 +02:00
if let error = error {
seal.reject(error)
} else {
guard let self = self, let sdp = sdp else { preconditionFailure() }
self.peerConnection.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't initiate call due to error: \(error).")
return seal.reject(error)
}
}
DispatchQueue.main.async {
let message = CallMessage()
2021-10-05 04:41:39 +02:00
message.uuid = self.uuid
message.kind = .offer
2021-08-17 08:02:20 +02:00
message.sdps = [ sdp.sdp ]
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
tsMessage.save(with: transaction)
2021-08-17 08:20:08 +02:00
MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 {
seal.fulfill(())
}.catch2 { error in
seal.reject(error)
}
}
2021-08-10 06:19:01 +02:00
}
}
return promise
}
public func sendAnswer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
2021-08-17 08:20:08 +02:00
print("[Calls] Sending answer message.")
guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) }
2021-08-10 06:19:01 +02:00
let (promise, seal) = Promise<Void>.pending()
peerConnection.answer(for: mediaConstraints) { [weak self] sdp, error in
2021-08-10 06:19:01 +02:00
if let error = error {
seal.reject(error)
} else {
guard let self = self, let sdp = sdp else { preconditionFailure() }
self.peerConnection.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't accept call due to error: \(error).")
return seal.reject(error)
}
}
DispatchQueue.main.async {
let message = CallMessage()
message.kind = .answer
2021-08-17 08:02:20 +02:00
message.sdps = [ sdp.sdp ]
2021-08-17 08:20:08 +02:00
MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 {
seal.fulfill(())
}.catch2 { error in
seal.reject(error)
}
}
2021-08-10 06:19:01 +02:00
}
}
return promise
}
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() {
Storage.write { transaction in
let candidates = self.queuedICECandidates
guard let thread = TSContactThread.fetch(for: self.contactSessionID, using: transaction) else { return }
2021-08-17 08:20:08 +02:00
print("[Calls] Batch sending \(candidates.count) ICE candidates.")
2021-08-17 08:02:20 +02:00
let message = CallMessage()
let sdps = candidates.map { $0.sdp }
let sdpMLineIndexes = candidates.map { UInt32($0.sdpMLineIndex) }
let sdpMids = candidates.map { $0.sdpMid! }
message.kind = .iceCandidates(sdpMLineIndexes: sdpMLineIndexes, sdpMids: sdpMids)
message.sdps = sdps
self.queuedICECandidates.removeAll()
2021-08-17 08:20:08 +02:00
MessageSender.sendNonDurably(message, in: thread, using: transaction).retainUntilComplete()
2021-08-17 08:02:20 +02:00
}
}
2021-08-18 02:33:33 +02:00
public func endCall(with sessionID: String, using transaction: YapDatabaseReadWriteTransaction) {
guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return }
let message = CallMessage()
message.kind = .endCall
2021-08-18 02:45:55 +02:00
print("[Calls] Sending end call message.")
2021-08-18 02:33:33 +02:00
MessageSender.sendNonDurably(message, in: thread, using: transaction).retainUntilComplete()
dropConnection()
WebRTCSession.current = nil
}
public func dropConnection() {
2021-08-10 06:19:01 +02:00
peerConnection.close()
}
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) {
print("[Calls] Signaling state changed to: \(state).")
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
2021-09-09 01:21:13 +02:00
print("[Calls] Peer connection did add stream.")
configureAudioSession()
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
2021-09-09 01:21:13 +02:00
print("[Calls] Peer connection did remove stream.")
2021-08-10 06:19:01 +02:00
}
public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
// Do nothing
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceConnectionState) {
print("[Calls] ICE connection state changed to: \(state).")
2021-09-29 03:17:48 +02:00
if state == .connected {
delegate?.webRTCDidConnected()
}
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceGatheringState) {
print("[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]) {
print("[Calls] \(candidates.count) ICE candidate(s) removed.")
2021-08-10 06:19:01 +02:00
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
print("[Calls] Data channel opened.")
2021-08-10 06:19:01 +02:00
}
}
2021-09-09 01:21:13 +02:00
extension WebRTCSession {
private func configureAudioSession() {
let audioSession = RTCAudioSession.sharedInstance()
audioSession.lockForConfiguration()
do {
try audioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue)
try audioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
try audioSession.overrideOutputAudioPort(.speaker)
try audioSession.setActive(true)
} catch let error {
SNLog("Couldn't set up WebRTC audio session due to error: \(error)")
}
audioSession.unlockForConfiguration()
}
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
}
public func turnOnVideo() {
localVideoTrack.isEnabled = true
}
2021-09-09 01:21:13 +02:00
}