parent
5929061291
commit
af289145b5
|
@ -15,10 +15,10 @@ import WebRTC
|
|||
* ## Signaling
|
||||
*
|
||||
* Signaling refers to the setup and tear down of the connection. Before the connection is established, this must happen
|
||||
* out of band (using Signal Service), but once the connection is established it's possible to publish updates
|
||||
* out of band (using Signal Service), but once the connection is established it's possible to publish updates
|
||||
* (like hangup) via the established channel.
|
||||
*
|
||||
* Signaling state is synchronized on the `signalingQueue` and only mutated in the handleXXX family of methods.
|
||||
* Signaling state is synchronized on the main thread and only mutated in the handleXXX family of methods.
|
||||
*
|
||||
* Following is a high level process of the exchange of messages that takes place during call signaling.
|
||||
*
|
||||
|
@ -34,7 +34,7 @@ import WebRTC
|
|||
* | Caller | Callee |
|
||||
* +----------------------------+-------------------------+
|
||||
* Start outgoing call: `handleOutgoingCall`...
|
||||
--[SS.CallOffer]-->
|
||||
--[SS.CallOffer]-->
|
||||
* ...and start generating ICE updates.
|
||||
* As ICE candidates are generated, `handleLocalAddedIceCandidate` is called.
|
||||
* and we *store* the ICE updates for later.
|
||||
|
@ -44,7 +44,7 @@ import WebRTC
|
|||
* <--[SS.CallAnswer]--
|
||||
* Start generating ICE updates.
|
||||
* As they are generated `handleLocalAddedIceCandidate` is called
|
||||
which immediately sends the ICE updates to the Caller.
|
||||
which immediately sends the ICE updates to the Caller.
|
||||
* <--[SS.ICEUpdate]-- (sent multiple times)
|
||||
*
|
||||
* Received CallAnswer: `handleReceivedAnswer`
|
||||
|
@ -89,8 +89,7 @@ protocol CallServiceObserver: class {
|
|||
remoteVideoTrack: RTCVideoTrack?)
|
||||
}
|
||||
|
||||
// This class' state should only be accessed on the signaling queue, _except_
|
||||
// the observer-related state which only be accessed on the main thread.
|
||||
// This class' state should only be accessed on the main queue.
|
||||
@objc class CallService: NSObject, CallObserver, PeerConnectionClientDelegate {
|
||||
|
||||
// MARK: - Properties
|
||||
|
@ -109,9 +108,6 @@ protocol CallServiceObserver: class {
|
|||
|
||||
static let fallbackIceServer = RTCIceServer(urlStrings: ["stun:stun1.l.google.com:19302"])
|
||||
|
||||
// Synchronize call signaling on the callSignalingQueue to make sure any appropriate requisite state is set.
|
||||
static let signalingQueue = DispatchQueue(label: "CallServiceSignalingQueue")
|
||||
|
||||
// MARK: Ivars
|
||||
|
||||
var peerConnectionClient: PeerConnectionClient?
|
||||
|
@ -119,23 +115,21 @@ protocol CallServiceObserver: class {
|
|||
var thread: TSContactThread?
|
||||
var call: SignalCall? {
|
||||
didSet {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
oldValue?.removeObserver(self)
|
||||
call?.addObserverAndSyncState(observer: self)
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.updateIsVideoEnabled()
|
||||
}
|
||||
updateIsVideoEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In the process of establishing a connection between the clients (ICE process) we must exchange ICE updates.
|
||||
* Because this happens via Signal Service it's possible the callee user has not accepted any change in the caller's
|
||||
* Because this happens via Signal Service it's possible the callee user has not accepted any change in the caller's
|
||||
* identity. In which case *each* ICE update would cause an "identity change" warning on the callee's device. Since
|
||||
* this could be several messages, the caller stores all ICE updates until receiving positive confirmation that the
|
||||
* callee has received a message from us. This positive confirmation comes in the form of the callees `CallAnswer`
|
||||
* this could be several messages, the caller stores all ICE updates until receiving positive confirmation that the
|
||||
* callee has received a message from us. This positive confirmation comes in the form of the callees `CallAnswer`
|
||||
* message.
|
||||
*/
|
||||
var sendIceUpdatesImmediately = true
|
||||
|
@ -149,7 +143,7 @@ protocol CallServiceObserver: class {
|
|||
|
||||
weak var localVideoTrack: RTCVideoTrack? {
|
||||
didSet {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.info("\(self.TAG) \(#function)")
|
||||
|
||||
|
@ -159,7 +153,7 @@ protocol CallServiceObserver: class {
|
|||
|
||||
weak var remoteVideoTrack: RTCVideoTrack? {
|
||||
didSet {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.info("\(self.TAG) \(#function)")
|
||||
|
||||
|
@ -168,7 +162,7 @@ protocol CallServiceObserver: class {
|
|||
}
|
||||
var isRemoteVideoEnabled = false {
|
||||
didSet {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.info("\(self.TAG) \(#function)")
|
||||
|
||||
|
@ -192,7 +186,7 @@ protocol CallServiceObserver: class {
|
|||
selector:#selector(didBecomeActive),
|
||||
name:NSNotification.Name.UIApplicationDidBecomeActive,
|
||||
object:nil)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
@ -223,14 +217,11 @@ protocol CallServiceObserver: class {
|
|||
|
||||
// MARK: - Service Actions
|
||||
|
||||
// Unless otherwise documented, these `handleXXX` methods expect to be called on the SignalingQueue to coordinate
|
||||
// state across calls.
|
||||
|
||||
/**
|
||||
* Initiate an outgoing call.
|
||||
*/
|
||||
public func handleOutgoingCall(_ call: SignalCall) -> Promise<Void> {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.call = call
|
||||
|
||||
|
@ -250,8 +241,9 @@ protocol CallServiceObserver: class {
|
|||
return Promise(error: CallError.assertionError(description: errorDescription))
|
||||
}
|
||||
|
||||
return getIceServers().then(on: CallService.signalingQueue) { iceServers -> Promise<HardenedRTCSessionDescription> in
|
||||
return getIceServers().then(on: DispatchQueue.main) { iceServers -> Promise<HardenedRTCSessionDescription> in
|
||||
Logger.debug("\(self.TAG) got ice servers:\(iceServers)")
|
||||
|
||||
let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self)
|
||||
|
||||
// When placing an outgoing call, it's our responsibility to create the DataChannel. Recipient will not have
|
||||
|
@ -260,22 +252,24 @@ protocol CallServiceObserver: class {
|
|||
|
||||
self.peerConnectionClient = peerConnectionClient
|
||||
|
||||
return self.peerConnectionClient!.createOffer()
|
||||
}.then(on: CallService.signalingQueue) { (sessionDescription: HardenedRTCSessionDescription) -> Promise<Void> in
|
||||
return self.peerConnectionClient!.setLocalSessionDescription(sessionDescription).then(on: CallService.signalingQueue) {
|
||||
let offerMessage = OWSCallOfferMessage(callId: call.signalingId, sessionDescription: sessionDescription.sdp)
|
||||
let callMessage = OWSOutgoingCallMessage(thread: thread, offerMessage: offerMessage)
|
||||
return self.messageSender.sendCallMessage(callMessage)
|
||||
}
|
||||
}.catch(on: CallService.signalingQueue) { error in
|
||||
Logger.error("\(self.TAG) placing call failed with error: \(error)")
|
||||
let sessionDescription = self.peerConnectionClient!.createOffer()
|
||||
return sessionDescription
|
||||
}.then(on: DispatchQueue.main) { (sessionDescription: HardenedRTCSessionDescription) -> Promise<Void> in
|
||||
return self.peerConnectionClient!.setLocalSessionDescription(sessionDescription).then(on: DispatchQueue.main) {
|
||||
let offerMessage = OWSCallOfferMessage(callId: call.signalingId, sessionDescription: sessionDescription.sdp)
|
||||
let callMessage = OWSOutgoingCallMessage(thread: thread, offerMessage: offerMessage)
|
||||
let result = self.messageSender.sendCallMessage(callMessage)
|
||||
return result
|
||||
}
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
Logger.error("\(self.TAG) placing call failed with error: \(error)")
|
||||
|
||||
if let callError = error as? CallError {
|
||||
self.handleFailedCall(error: callError)
|
||||
} else {
|
||||
let externalError = CallError.externalError(underlyingError: error)
|
||||
self.handleFailedCall(error: externalError)
|
||||
}
|
||||
if let callError = error as? CallError {
|
||||
self.handleFailedCall(error: callError)
|
||||
} else {
|
||||
let externalError = CallError.externalError(underlyingError: error)
|
||||
self.handleFailedCall(error: externalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,7 +278,7 @@ protocol CallServiceObserver: class {
|
|||
*/
|
||||
public func handleReceivedAnswer(thread: TSContactThread, callId: UInt64, sessionDescription: String) {
|
||||
Logger.debug("\(TAG) received call answer for call: \(callId) thread: \(thread)")
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let call = self.call else {
|
||||
handleFailedCall(error: .assertionError(description:"call was unexpectedly nil in \(#function)"))
|
||||
|
@ -315,13 +309,13 @@ protocol CallServiceObserver: class {
|
|||
let sessionDescription = RTCSessionDescription(type: .answer, sdp: sessionDescription)
|
||||
_ = peerConnectionClient.setRemoteSessionDescription(sessionDescription).then {
|
||||
Logger.debug("\(self.TAG) successfully set remote description")
|
||||
}.catch(on: CallService.signalingQueue) { error in
|
||||
if let callError = error as? CallError {
|
||||
self.handleFailedCall(error: callError)
|
||||
} else {
|
||||
let externalError = CallError.externalError(underlyingError: error)
|
||||
self.handleFailedCall(error: externalError)
|
||||
}
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
if let callError = error as? CallError {
|
||||
self.handleFailedCall(error: callError)
|
||||
} else {
|
||||
let externalError = CallError.externalError(underlyingError: error)
|
||||
self.handleFailedCall(error: externalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,6 +323,7 @@ protocol CallServiceObserver: class {
|
|||
* User didn't answer incoming call
|
||||
*/
|
||||
public func handleMissedCall(_ call: SignalCall, thread: TSContactThread) {
|
||||
AssertIsOnMainThread()
|
||||
// Insert missed call record
|
||||
let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
withCallNumber: thread.contactIdentifier(),
|
||||
|
@ -336,9 +331,7 @@ protocol CallServiceObserver: class {
|
|||
in: thread)
|
||||
callRecord.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.callUIAdapter.reportMissedCall(call)
|
||||
}
|
||||
self.callUIAdapter.reportMissedCall(call)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -346,7 +339,7 @@ protocol CallServiceObserver: class {
|
|||
*/
|
||||
private func handleLocalBusyCall(_ call: SignalCall, thread: TSContactThread) {
|
||||
Logger.debug("\(TAG) \(#function) for call: \(call) thread: \(thread)")
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let busyMessage = OWSCallBusyMessage(callId: call.signalingId)
|
||||
let callMessage = OWSOutgoingCallMessage(thread: thread, busyMessage: busyMessage)
|
||||
|
@ -360,7 +353,7 @@ protocol CallServiceObserver: class {
|
|||
*/
|
||||
public func handleRemoteBusy(thread: TSContactThread) {
|
||||
Logger.debug("\(TAG) \(#function) for thread: \(thread)")
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let call = self.call else {
|
||||
handleFailedCall(error: .assertionError(description: "call unexpectedly nil in \(#function)"))
|
||||
|
@ -376,7 +369,7 @@ protocol CallServiceObserver: class {
|
|||
* the user of an incoming call.
|
||||
*/
|
||||
public func handleReceivedOffer(thread: TSContactThread, callId: UInt64, sessionDescription callerSessionDescription: String) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.verbose("\(TAG) receivedCallOffer for thread:\(thread)")
|
||||
let newCall = SignalCall.incomingCall(localId: UUID(), remotePhoneNumber: thread.contactIdentifier(), signalingId: callId)
|
||||
|
@ -394,54 +387,54 @@ protocol CallServiceObserver: class {
|
|||
|
||||
let backgroundTask = UIApplication.shared.beginBackgroundTask {
|
||||
let timeout = CallError.timeout(description: "background task time ran out before call connected.")
|
||||
CallService.signalingQueue.async {
|
||||
DispatchQueue.main.async {
|
||||
self.handleFailedCall(error: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
incomingCallPromise = firstly {
|
||||
return getIceServers()
|
||||
}.then(on: CallService.signalingQueue) { (iceServers: [RTCIceServer]) -> Promise<HardenedRTCSessionDescription> in
|
||||
// FIXME for first time call recipients I think we'll see mic/camera permission requests here,
|
||||
// even though, from the users perspective, no incoming call is yet visible.
|
||||
self.peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self)
|
||||
}.then(on: DispatchQueue.main) { (iceServers: [RTCIceServer]) -> Promise<HardenedRTCSessionDescription> in
|
||||
// FIXME for first time call recipients I think we'll see mic/camera permission requests here,
|
||||
// even though, from the users perspective, no incoming call is yet visible.
|
||||
self.peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self)
|
||||
|
||||
let offerSessionDescription = RTCSessionDescription(type: .offer, sdp: callerSessionDescription)
|
||||
let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
|
||||
let offerSessionDescription = RTCSessionDescription(type: .offer, sdp: callerSessionDescription)
|
||||
let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
|
||||
|
||||
// Find a sessionDescription compatible with my constraints and the remote sessionDescription
|
||||
return self.peerConnectionClient!.negotiateSessionDescription(remoteDescription: offerSessionDescription, constraints: constraints)
|
||||
}.then(on: CallService.signalingQueue) { (negotiatedSessionDescription: HardenedRTCSessionDescription) in
|
||||
Logger.debug("\(self.TAG) set the remote description")
|
||||
// Find a sessionDescription compatible with my constraints and the remote sessionDescription
|
||||
return self.peerConnectionClient!.negotiateSessionDescription(remoteDescription: offerSessionDescription, constraints: constraints)
|
||||
}.then(on: DispatchQueue.main) { (negotiatedSessionDescription: HardenedRTCSessionDescription) in
|
||||
Logger.debug("\(self.TAG) set the remote description")
|
||||
|
||||
let answerMessage = OWSCallAnswerMessage(callId: newCall.signalingId, sessionDescription: negotiatedSessionDescription.sdp)
|
||||
let callAnswerMessage = OWSOutgoingCallMessage(thread: thread, answerMessage: answerMessage)
|
||||
let answerMessage = OWSCallAnswerMessage(callId: newCall.signalingId, sessionDescription: negotiatedSessionDescription.sdp)
|
||||
let callAnswerMessage = OWSOutgoingCallMessage(thread: thread, answerMessage: answerMessage)
|
||||
|
||||
return self.messageSender.sendCallMessage(callAnswerMessage)
|
||||
}.then(on: CallService.signalingQueue) {
|
||||
Logger.debug("\(self.TAG) successfully sent callAnswerMessage")
|
||||
return self.messageSender.sendCallMessage(callAnswerMessage)
|
||||
}.then(on: DispatchQueue.main) {
|
||||
Logger.debug("\(self.TAG) successfully sent callAnswerMessage")
|
||||
|
||||
let (promise, fulfill, _) = Promise<Void>.pending()
|
||||
let (promise, fulfill, _) = Promise<Void>.pending()
|
||||
|
||||
let timeout: Promise<Void> = after(interval: TimeInterval(timeoutSeconds)).then { () -> Void in
|
||||
// rejecting a promise by throwing is safely a no-op if the promise has already been fulfilled
|
||||
throw CallError.timeout(description: "timed out waiting for call to connect")
|
||||
}
|
||||
let timeout: Promise<Void> = after(interval: TimeInterval(timeoutSeconds)).then { () -> Void in
|
||||
// rejecting a promise by throwing is safely a no-op if the promise has already been fulfilled
|
||||
throw CallError.timeout(description: "timed out waiting for call to connect")
|
||||
}
|
||||
|
||||
// This will be fulfilled (potentially) by the RTCDataChannel delegate method
|
||||
self.fulfillCallConnectedPromise = fulfill
|
||||
// This will be fulfilled (potentially) by the RTCDataChannel delegate method
|
||||
self.fulfillCallConnectedPromise = fulfill
|
||||
|
||||
return race(promise, timeout)
|
||||
}.catch(on: CallService.signalingQueue) { error in
|
||||
if let callError = error as? CallError {
|
||||
self.handleFailedCall(error: callError)
|
||||
} else {
|
||||
let externalError = CallError.externalError(underlyingError: error)
|
||||
self.handleFailedCall(error: externalError)
|
||||
}
|
||||
}.always {
|
||||
Logger.debug("\(self.TAG) ending background task awaiting inbound call connection")
|
||||
UIApplication.shared.endBackgroundTask(backgroundTask)
|
||||
return race(promise, timeout)
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
if let callError = error as? CallError {
|
||||
self.handleFailedCall(error: callError)
|
||||
} else {
|
||||
let externalError = CallError.externalError(underlyingError: error)
|
||||
self.handleFailedCall(error: externalError)
|
||||
}
|
||||
}.always {
|
||||
Logger.debug("\(self.TAG) ending background task awaiting inbound call connection")
|
||||
UIApplication.shared.endBackgroundTask(backgroundTask)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -449,7 +442,7 @@ protocol CallServiceObserver: class {
|
|||
* Remote client (could be caller or callee) sent us a connectivity update
|
||||
*/
|
||||
public func handleRemoteAddedIceCandidate(thread: TSContactThread, callId: UInt64, sdp: String, lineIndex: Int32, mid: String) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
Logger.debug("\(TAG) called \(#function)")
|
||||
|
||||
guard self.thread != nil else {
|
||||
|
@ -481,11 +474,11 @@ protocol CallServiceObserver: class {
|
|||
}
|
||||
|
||||
/**
|
||||
* Local client (could be caller or callee) generated some connectivity information that we should send to the
|
||||
* Local client (could be caller or callee) generated some connectivity information that we should send to the
|
||||
* remote client.
|
||||
*/
|
||||
private func handleLocalAddedIceCandidate(_ iceCandidate: RTCIceCandidate) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let call = self.call else {
|
||||
handleFailedCall(error: .assertionError(description: "ignoring local ice candidate, since there is no current call."))
|
||||
|
@ -520,11 +513,11 @@ protocol CallServiceObserver: class {
|
|||
/**
|
||||
* The clients can now communicate via WebRTC.
|
||||
*
|
||||
* Called by both caller and callee. Compatible ICE messages have been exchanged between the local and remote
|
||||
* Called by both caller and callee. Compatible ICE messages have been exchanged between the local and remote
|
||||
* client.
|
||||
*/
|
||||
private func handleIceConnected() {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
|
||||
|
@ -543,9 +536,7 @@ protocol CallServiceObserver: class {
|
|||
call.state = .remoteRinging
|
||||
case .answering:
|
||||
call.state = .localRinging
|
||||
DispatchQueue.main.async {
|
||||
self.callUIAdapter.reportIncomingCall(call, thread: thread)
|
||||
}
|
||||
self.callUIAdapter.reportIncomingCall(call, thread: thread)
|
||||
// cancel connection timeout
|
||||
self.fulfillCallConnectedPromise?()
|
||||
case .remoteRinging:
|
||||
|
@ -562,10 +553,10 @@ protocol CallServiceObserver: class {
|
|||
*/
|
||||
public func handleRemoteHangup(thread: TSContactThread) {
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard thread.contactIdentifier() == self.thread?.contactIdentifier() else {
|
||||
// This can safely be ignored.
|
||||
// This can safely be ignored.
|
||||
// We don't want to fail the current call because an old call was slow to send us the hangup message.
|
||||
Logger.warn("\(TAG) ignoring hangup for thread:\(thread) which is not the current thread: \(self.thread)")
|
||||
return
|
||||
|
@ -585,9 +576,7 @@ protocol CallServiceObserver: class {
|
|||
|
||||
call.state = .remoteHangup
|
||||
// Notify UI
|
||||
DispatchQueue.main.async {
|
||||
self.callUIAdapter.remoteDidHangupCall(call)
|
||||
}
|
||||
callUIAdapter.remoteDidHangupCall(call)
|
||||
|
||||
// self.call is nil'd in `terminateCall`, so it's important we update it's state *before* calling `terminateCall`
|
||||
terminateCall()
|
||||
|
@ -599,8 +588,7 @@ protocol CallServiceObserver: class {
|
|||
* Used by notification actions which can't serialize a call object.
|
||||
*/
|
||||
public func handleAnswerCall(localId: UUID) {
|
||||
// TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue)
|
||||
//assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let call = self.call else {
|
||||
handleFailedCall(error: .assertionError(description:"\(TAG) call was unexpectedly nil in \(#function)"))
|
||||
|
@ -612,18 +600,14 @@ protocol CallServiceObserver: class {
|
|||
return
|
||||
}
|
||||
|
||||
// Because we may not be on signalingQueue (because this method is called from Objc which doesn't have
|
||||
// access to signalingQueue (that I can find). FIXME?
|
||||
type(of: self).signalingQueue.async {
|
||||
self.handleAnswerCall(call)
|
||||
}
|
||||
self.handleAnswerCall(call)
|
||||
}
|
||||
|
||||
/**
|
||||
* User chose to answer call referrred to by call `localId`. Used by the Callee only.
|
||||
*/
|
||||
public func handleAnswerCall(_ call: SignalCall) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
|
||||
|
@ -668,7 +652,7 @@ protocol CallServiceObserver: class {
|
|||
*/
|
||||
func handleConnectedCall(_ call: SignalCall) {
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let peerConnectionClient = self.peerConnectionClient else {
|
||||
handleFailedCall(error: .assertionError(description:"\(TAG) peerConnectionClient unexpectedly nil in \(#function)"))
|
||||
|
@ -690,8 +674,7 @@ protocol CallServiceObserver: class {
|
|||
* Incoming call only.
|
||||
*/
|
||||
public func handleDeclineCall(localId: UUID) {
|
||||
// #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue)
|
||||
//assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let call = self.call else {
|
||||
handleFailedCall(error: .assertionError(description:"\(TAG) call was unexpectedly nil in \(#function)"))
|
||||
|
@ -703,11 +686,7 @@ protocol CallServiceObserver: class {
|
|||
return
|
||||
}
|
||||
|
||||
// Because we may not be on signalingQueue (because this method is called from Objc which doesn't have
|
||||
// access to signalingQueue (that I can find). FIXME?
|
||||
type(of: self).signalingQueue.async {
|
||||
self.handleDeclineCall(call)
|
||||
}
|
||||
self.handleDeclineCall(call)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -716,7 +695,7 @@ protocol CallServiceObserver: class {
|
|||
* Incoming call only.
|
||||
*/
|
||||
public func handleDeclineCall(_ call: SignalCall) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.info("\(TAG) in \(#function)")
|
||||
|
||||
|
@ -730,7 +709,7 @@ protocol CallServiceObserver: class {
|
|||
* Can be used for Incoming and Outgoing calls.
|
||||
*/
|
||||
func handleLocalHungupCall(_ call: SignalCall) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard self.call != nil else {
|
||||
handleFailedCall(error: .assertionError(description:"\(TAG) ignoring \(#function) since there is no current call"))
|
||||
|
@ -769,10 +748,10 @@ protocol CallServiceObserver: class {
|
|||
// If the call hasn't started yet, we don't have a data channel to communicate the hang up. Use Signal Service Message.
|
||||
let hangupMessage = OWSCallHangupMessage(callId: call.signalingId)
|
||||
let callMessage = OWSOutgoingCallMessage(thread: thread, hangupMessage: hangupMessage)
|
||||
_ = self.messageSender.sendCallMessage(callMessage).then(on: CallService.signalingQueue) {
|
||||
_ = self.messageSender.sendCallMessage(callMessage).then(on: DispatchQueue.main) {
|
||||
Logger.debug("\(self.TAG) successfully sent hangup call message to \(thread)")
|
||||
}.catch(on: CallService.signalingQueue) { error in
|
||||
Logger.error("\(self.TAG) failed to send hangup call message to \(thread) with error: \(error)")
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
Logger.error("\(self.TAG) failed to send hangup call message to \(thread) with error: \(error)")
|
||||
}
|
||||
|
||||
terminateCall()
|
||||
|
@ -784,7 +763,7 @@ protocol CallServiceObserver: class {
|
|||
* Can be used for Incoming and Outgoing calls.
|
||||
*/
|
||||
func setIsMuted(isMuted: Bool) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let peerConnectionClient = self.peerConnectionClient else {
|
||||
handleFailedCall(error: .assertionError(description:"\(TAG) peerConnectionClient unexpectedly nil in \(#function)"))
|
||||
|
@ -806,7 +785,7 @@ protocol CallServiceObserver: class {
|
|||
* Can be used for Incoming and Outgoing calls.
|
||||
*/
|
||||
func setHasLocalVideo(hasLocalVideo: Bool) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let authStatus = AVCaptureDevice.authorizationStatus(forMediaType:AVMediaTypeVideo)
|
||||
switch authStatus {
|
||||
|
@ -828,14 +807,12 @@ protocol CallServiceObserver: class {
|
|||
// during a call while the app is in the background, because changing this
|
||||
// permission kills the app.
|
||||
if authStatus != .authorized {
|
||||
DispatchQueue.main.async {
|
||||
let title = NSLocalizedString("MISSING_CAMERA_PERMISSION_TITLE", comment: "Alert title when camera is not authorized")
|
||||
let message = NSLocalizedString("MISSING_CAMERA_PERMISSION_MESSAGE", comment: "Alert body when camera is not authorized")
|
||||
let okButton = NSLocalizedString("OK", comment:"")
|
||||
let title = NSLocalizedString("MISSING_CAMERA_PERMISSION_TITLE", comment: "Alert title when camera is not authorized")
|
||||
let message = NSLocalizedString("MISSING_CAMERA_PERMISSION_MESSAGE", comment: "Alert body when camera is not authorized")
|
||||
let okButton = NSLocalizedString("OK", comment:"")
|
||||
|
||||
let alert = UIAlertView(title:title, message:message, delegate:nil, cancelButtonTitle:okButton)
|
||||
alert.show()
|
||||
}
|
||||
let alert = UIAlertView(title:title, message:message, delegate:nil, cancelButtonTitle:okButton)
|
||||
alert.show()
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -855,23 +832,23 @@ protocol CallServiceObserver: class {
|
|||
}
|
||||
|
||||
func handleCallKitStartVideo() {
|
||||
CallService.signalingQueue.async {
|
||||
self.setHasLocalVideo(hasLocalVideo:true)
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.setHasLocalVideo(hasLocalVideo:true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Local client received a message on the WebRTC data channel.
|
||||
* Local client received a message on the WebRTC data channel.
|
||||
*
|
||||
* The WebRTC data channel is a faster signaling channel than out of band Signal Service messages. Once it's
|
||||
* established we use it to communicate further signaling information. The one sort-of exception is that with
|
||||
* hangup messages we redundantly send a Signal Service hangup message, which is more reliable, and since the hangup
|
||||
* The WebRTC data channel is a faster signaling channel than out of band Signal Service messages. Once it's
|
||||
* established we use it to communicate further signaling information. The one sort-of exception is that with
|
||||
* hangup messages we redundantly send a Signal Service hangup message, which is more reliable, and since the hangup
|
||||
* action is idemptotent, there's no harm done.
|
||||
*
|
||||
* Used by both Incoming and Outgoing calls.
|
||||
*/
|
||||
private func handleDataChannelMessage(_ message: OWSWebRTCProtosData) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let call = self.call else {
|
||||
handleFailedCall(error: .assertionError(description:"\(TAG) received data message, but there is no current call. Ignoring."))
|
||||
|
@ -888,9 +865,7 @@ protocol CallServiceObserver: class {
|
|||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.callUIAdapter.recipientAcceptedCall(call)
|
||||
}
|
||||
self.callUIAdapter.recipientAcceptedCall(call)
|
||||
handleConnectedCall(call)
|
||||
|
||||
} else if message.hasHangup() {
|
||||
|
@ -922,18 +897,18 @@ protocol CallServiceObserver: class {
|
|||
* The connection has been established. The clients can now communicate.
|
||||
*/
|
||||
internal func peerConnectionClientIceConnected(_ peerconnectionClient: PeerConnectionClient) {
|
||||
CallService.signalingQueue.async {
|
||||
self.handleIceConnected()
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.handleIceConnected()
|
||||
}
|
||||
|
||||
/**
|
||||
* The connection failed to establish. The clients will not be able to communicate.
|
||||
*/
|
||||
internal func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient) {
|
||||
CallService.signalingQueue.async {
|
||||
self.handleFailedCall(error: CallError.disconnected)
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.handleFailedCall(error: CallError.disconnected)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -942,94 +917,75 @@ protocol CallServiceObserver: class {
|
|||
* out of band, as part of establishing a connection over WebRTC.
|
||||
*/
|
||||
internal func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate) {
|
||||
CallService.signalingQueue.async {
|
||||
self.handleLocalAddedIceCandidate(iceCandidate)
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.handleLocalAddedIceCandidate(iceCandidate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the peerconnection is established, we can receive messages via the data channel, and notify the delegate.
|
||||
*/
|
||||
internal func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, received dataChannelMessage: OWSWebRTCProtosData) {
|
||||
CallService.signalingQueue.async {
|
||||
self.handleDataChannelMessage(dataChannelMessage)
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.handleDataChannelMessage(dataChannelMessage)
|
||||
}
|
||||
|
||||
internal func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateLocal videoTrack: RTCVideoTrack?) {
|
||||
CallService.signalingQueue.async { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.localVideoTrack = videoTrack
|
||||
strongSelf.fireDidUpdateVideoTracks()
|
||||
}
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.localVideoTrack = videoTrack
|
||||
self.fireDidUpdateVideoTracks()
|
||||
}
|
||||
|
||||
internal func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateRemote videoTrack: RTCVideoTrack?) {
|
||||
CallService.signalingQueue.async { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.remoteVideoTrack = videoTrack
|
||||
strongSelf.fireDidUpdateVideoTracks()
|
||||
}
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.remoteVideoTrack = videoTrack
|
||||
self.fireDidUpdateVideoTracks()
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
/**
|
||||
* Ensure that all `SignalCall` and `CallService` state is synchronized by only mutating signaling state in
|
||||
* handleXXX methods, and putting those methods on the signaling queue.
|
||||
*
|
||||
* TODO: We might want to move this queue and method to OWSDispatch so that we can assert this in
|
||||
* other classes like SignalCall as well.
|
||||
*/
|
||||
private func assertOnSignalingQueue() {
|
||||
if #available(iOS 10.0, *) {
|
||||
dispatchPrecondition(condition: .onQueue(type(of: self).signalingQueue))
|
||||
} else {
|
||||
// Skipping check on <iOS10, since syntax is different and it's just a development convenience.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RTCIceServers are used when attempting to establish an optimal connection to the other party. SignalService supplies
|
||||
* a list of servers, plus we have fallback servers hardcoded in the app.
|
||||
*/
|
||||
private func getIceServers() -> Promise<[RTCIceServer]> {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return firstly {
|
||||
return accountManager.getTurnServerInfo()
|
||||
}.then(on: CallService.signalingQueue) { turnServerInfo -> [RTCIceServer] in
|
||||
Logger.debug("\(self.TAG) got turn server urls: \(turnServerInfo.urls)")
|
||||
}.then(on: DispatchQueue.main) { turnServerInfo -> [RTCIceServer] in
|
||||
Logger.debug("\(self.TAG) got turn server urls: \(turnServerInfo.urls)")
|
||||
|
||||
return turnServerInfo.urls.map { url in
|
||||
if url.hasPrefix("turn") {
|
||||
// Only "turn:" servers require authentication. Don't include the credentials to other ICE servers
|
||||
// as 1.) they aren't used, and 2.) the non-turn servers might not be under our control.
|
||||
// e.g. we use a public fallback STUN server.
|
||||
return RTCIceServer(urlStrings: [url], username: turnServerInfo.username, credential: turnServerInfo.password)
|
||||
} else {
|
||||
return RTCIceServer(urlStrings: [url])
|
||||
}
|
||||
} + [CallService.fallbackIceServer]
|
||||
}.recover { error -> [RTCIceServer] in
|
||||
Logger.error("\(self.TAG) fetching ICE servers failed with error: \(error)")
|
||||
Logger.warn("\(self.TAG) using fallback ICE Servers")
|
||||
return turnServerInfo.urls.map { url in
|
||||
if url.hasPrefix("turn") {
|
||||
// Only "turn:" servers require authentication. Don't include the credentials to other ICE servers
|
||||
// as 1.) they aren't used, and 2.) the non-turn servers might not be under our control.
|
||||
// e.g. we use a public fallback STUN server.
|
||||
return RTCIceServer(urlStrings: [url], username: turnServerInfo.username, credential: turnServerInfo.password)
|
||||
} else {
|
||||
return RTCIceServer(urlStrings: [url])
|
||||
}
|
||||
} + [CallService.fallbackIceServer]
|
||||
}.recover { error -> [RTCIceServer] in
|
||||
Logger.error("\(self.TAG) fetching ICE servers failed with error: \(error)")
|
||||
Logger.warn("\(self.TAG) using fallback ICE Servers")
|
||||
|
||||
return [CallService.fallbackIceServer]
|
||||
return [CallService.fallbackIceServer]
|
||||
}
|
||||
}
|
||||
|
||||
public func handleFailedCall(error: CallError) {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
Logger.error("\(TAG) call failed with error: \(error)")
|
||||
|
||||
if let call = self.call {
|
||||
// It's essential to set call.state before terminateCall, because terminateCall nils self.call
|
||||
call.error = error
|
||||
call.state = .localFailure
|
||||
DispatchQueue.main.async {
|
||||
self.callUIAdapter.failCall(call, error: error)
|
||||
}
|
||||
self.callUIAdapter.failCall(call, error: error)
|
||||
} else {
|
||||
// This can happen when we receive an out of band signaling message (e.g. IceUpdate)
|
||||
// after the call has ended
|
||||
|
@ -1043,12 +999,12 @@ protocol CallServiceObserver: class {
|
|||
* Clean up any existing call state and get ready to receive a new call.
|
||||
*/
|
||||
private func terminateCall() {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
|
||||
PeerConnectionClient.stopAudioSession()
|
||||
peerConnectionClient?.delegate = nil
|
||||
peerConnectionClient?.setDelegate(delegate:nil)
|
||||
peerConnectionClient?.terminate()
|
||||
|
||||
peerConnectionClient = nil
|
||||
|
@ -1092,7 +1048,7 @@ protocol CallServiceObserver: class {
|
|||
// MARK: - Video
|
||||
|
||||
private func shouldHaveLocalVideoTrack() -> Bool {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// The iOS simulator doesn't provide any sort of camera capture
|
||||
// support or emulation (http://goo.gl/rHAnC1) so don't bother
|
||||
|
@ -1107,29 +1063,24 @@ protocol CallServiceObserver: class {
|
|||
private func updateIsVideoEnabled() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// It's only safe to access the class properties on the signaling queue, so
|
||||
// we dispatch there...
|
||||
CallService.signalingQueue.async {
|
||||
guard let call = self.call else {
|
||||
return
|
||||
}
|
||||
guard let peerConnectionClient = self.peerConnectionClient else {
|
||||
return
|
||||
}
|
||||
guard let call = self.call else {
|
||||
return
|
||||
}
|
||||
guard let peerConnectionClient = self.peerConnectionClient else {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldHaveLocalVideoTrack = self.shouldHaveLocalVideoTrack()
|
||||
let shouldHaveLocalVideoTrack = self.shouldHaveLocalVideoTrack()
|
||||
|
||||
Logger.info("\(self.TAG) \(#function): \(shouldHaveLocalVideoTrack)")
|
||||
Logger.info("\(self.TAG) \(#function): \(shouldHaveLocalVideoTrack)")
|
||||
|
||||
self.peerConnectionClient?.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack)
|
||||
|
||||
let message = DataChannelMessage.forVideoStreamingStatus(callId: call.signalingId, enabled:shouldHaveLocalVideoTrack)
|
||||
if peerConnectionClient.sendDataChannelMessage(data: message.asData()) {
|
||||
Logger.debug("\(self.TAG) sendDataChannelMessage returned true")
|
||||
} else {
|
||||
Logger.warn("\(self.TAG) sendDataChannelMessage returned false")
|
||||
}
|
||||
self.peerConnectionClient?.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack)
|
||||
|
||||
let message = DataChannelMessage.forVideoStreamingStatus(callId: call.signalingId, enabled:shouldHaveLocalVideoTrack)
|
||||
if peerConnectionClient.sendDataChannelMessage(data: message.asData()) {
|
||||
Logger.debug("\(self.TAG) sendDataChannelMessage returned true")
|
||||
} else {
|
||||
Logger.warn("\(self.TAG) sendDataChannelMessage returned false")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1142,18 +1093,10 @@ protocol CallServiceObserver: class {
|
|||
observers.append(Weak(value: observer))
|
||||
|
||||
// Synchronize observer with current call state
|
||||
|
||||
// It's only safe to access the video track properties on the signaling queue, so
|
||||
// we dispatch there...
|
||||
CallService.signalingQueue.async {
|
||||
let localVideoTrack = self.localVideoTrack
|
||||
let remoteVideoTrack = self.isRemoteVideoEnabled ? self.remoteVideoTrack : nil
|
||||
// Then dispatch back to the main thread.
|
||||
DispatchQueue.main.async {
|
||||
observer.didUpdateVideoTracks(localVideoTrack:localVideoTrack,
|
||||
remoteVideoTrack:remoteVideoTrack)
|
||||
}
|
||||
}
|
||||
let localVideoTrack = self.localVideoTrack
|
||||
let remoteVideoTrack = self.isRemoteVideoEnabled ? self.remoteVideoTrack : nil
|
||||
observer.didUpdateVideoTracks(localVideoTrack:localVideoTrack,
|
||||
remoteVideoTrack:remoteVideoTrack)
|
||||
}
|
||||
|
||||
// The observer-related methods should be invoked on the main thread.
|
||||
|
@ -1173,27 +1116,23 @@ protocol CallServiceObserver: class {
|
|||
}
|
||||
|
||||
func fireDidUpdateVideoTracks() {
|
||||
assertOnSignalingQueue()
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let localVideoTrack = self.localVideoTrack
|
||||
let remoteVideoTrack = self.isRemoteVideoEnabled ? self.remoteVideoTrack : nil
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
if let strongSelf = self {
|
||||
for observer in strongSelf.observers {
|
||||
observer.value?.didUpdateVideoTracks(localVideoTrack:localVideoTrack,
|
||||
remoteVideoTrack:remoteVideoTrack)
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent screen from dimming during video call.
|
||||
//
|
||||
// fireDidUpdateVideoTracks() is called by the video track setters,
|
||||
// which are cleared when the call ends. That ensures that this timer
|
||||
// will be re-enabled.
|
||||
let hasLocalOrRemoteVideo = localVideoTrack != nil || remoteVideoTrack != nil
|
||||
UIApplication.shared.isIdleTimerDisabled = hasLocalOrRemoteVideo
|
||||
for observer in observers {
|
||||
observer.value?.didUpdateVideoTracks(localVideoTrack:localVideoTrack,
|
||||
remoteVideoTrack:remoteVideoTrack)
|
||||
}
|
||||
|
||||
// Prevent screen from dimming during video call.
|
||||
//
|
||||
// fireDidUpdateVideoTracks() is called by the video track setters,
|
||||
// which are cleared when the call ends. That ensures that this timer
|
||||
// will be re-enabled.
|
||||
let hasLocalOrRemoteVideo = localVideoTrack != nil || remoteVideoTrack != nil
|
||||
UIApplication.shared.isIdleTimerDisabled = hasLocalOrRemoteVideo
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,12 +27,10 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
|
|||
|
||||
let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle)
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
_ = self.callService.handleOutgoingCall(call).then {
|
||||
Logger.debug("\(self.TAG) handleOutgoingCall succeeded")
|
||||
self.callService.handleOutgoingCall(call).then {
|
||||
Logger.debug("\(self.TAG) handleOutgoingCall succeeded")
|
||||
}.catch { error in
|
||||
Logger.error("\(self.TAG) handleOutgoingCall failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
return call
|
||||
|
@ -64,64 +62,56 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
|
|||
func answerCall(localId: UUID) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
guard let call = self.callService.call else {
|
||||
assertionFailure("\(self.TAG) in \(#function) No current call.")
|
||||
return
|
||||
}
|
||||
|
||||
guard call.localId == localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.answerCall(call)
|
||||
guard let call = self.callService.call else {
|
||||
assertionFailure("\(self.TAG) in \(#function) No current call.")
|
||||
return
|
||||
}
|
||||
|
||||
guard call.localId == localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.answerCall(call)
|
||||
}
|
||||
|
||||
func answerCall(_ call: SignalCall) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
PeerConnectionClient.startAudioSession()
|
||||
self.callService.handleAnswerCall(call)
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
PeerConnectionClient.startAudioSession()
|
||||
self.callService.handleAnswerCall(call)
|
||||
}
|
||||
|
||||
func declineCall(localId: UUID) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
guard let call = self.callService.call else {
|
||||
assertionFailure("\(self.TAG) in \(#function) No current call.")
|
||||
return
|
||||
}
|
||||
|
||||
guard call.localId == localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.declineCall(call)
|
||||
guard let call = self.callService.call else {
|
||||
assertionFailure("\(self.TAG) in \(#function) No current call.")
|
||||
return
|
||||
}
|
||||
|
||||
guard call.localId == localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.declineCall(call)
|
||||
}
|
||||
|
||||
func declineCall(_ call: SignalCall) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.handleDeclineCall(call)
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.handleDeclineCall(call)
|
||||
}
|
||||
|
||||
func recipientAcceptedCall(_ call: SignalCall) {
|
||||
|
@ -133,16 +123,14 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
|
|||
func localHangupCall(_ call: SignalCall) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
// If both parties hang up at the same moment,
|
||||
// call might already be nil.
|
||||
guard self.callService.call == nil || call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.handleLocalHungupCall(call)
|
||||
// If both parties hang up at the same moment,
|
||||
// call might already be nil.
|
||||
guard self.callService.call == nil || call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.handleLocalHungupCall(call)
|
||||
}
|
||||
|
||||
internal func remoteDidHangupCall(_ call: SignalCall) {
|
||||
|
@ -160,26 +148,22 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
|
|||
func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.setIsMuted(isMuted: isMuted)
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.setIsMuted(isMuted: isMuted)
|
||||
}
|
||||
|
||||
func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.setHasLocalVideo(hasLocalVideo: hasLocalVideo)
|
||||
guard call.localId == self.callService.call?.localId else {
|
||||
assertionFailure("\(self.TAG) in \(#function) localId does not match current call")
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.setHasLocalVideo(hasLocalVideo: hasLocalVideo)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ protocol PeerConnectionClientDelegate: class {
|
|||
func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient)
|
||||
|
||||
/**
|
||||
* During the Signaling process each client generates IceCandidates locally, which contain information about how to
|
||||
* reach the local client via the internet. The delegate must shuttle these IceCandates to the other (remote) client
|
||||
* During the Signaling process each client generates IceCandidates locally, which contain information about how to
|
||||
* reach the local client via the internet. The delegate must shuttle these IceCandates to the other (remote) client
|
||||
* out of band, as part of establishing a connection over WebRTC.
|
||||
*/
|
||||
func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate)
|
||||
|
@ -57,7 +57,7 @@ protocol PeerConnectionClientDelegate: class {
|
|||
/**
|
||||
* `PeerConnectionClient` is our interface to WebRTC.
|
||||
*
|
||||
* It is primarily a wrapper around `RTCPeerConnection`, which is responsible for sending and receiving our call data
|
||||
* It is primarily a wrapper around `RTCPeerConnection`, which is responsible for sending and receiving our call data
|
||||
* including audio, video, and some post-connected signaling (hangup, add video)
|
||||
*/
|
||||
class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDelegate {
|
||||
|
@ -65,17 +65,29 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
let TAG = "[PeerConnectionClient]"
|
||||
enum Identifiers: String {
|
||||
case mediaStream = "ARDAMS",
|
||||
videoTrack = "ARDAMSv0",
|
||||
audioTrack = "ARDAMSa0",
|
||||
dataChannelSignaling = "signaling"
|
||||
videoTrack = "ARDAMSv0",
|
||||
audioTrack = "ARDAMSa0",
|
||||
dataChannelSignaling = "signaling"
|
||||
}
|
||||
|
||||
// A state in this class should only be accessed on this queue in order to
|
||||
// serialize access.
|
||||
//
|
||||
// This queue is also used to perform expensive calls to the WebRTC API.
|
||||
private static let signalingQueue = DispatchQueue(label: "CallServiceSignalingQueue")
|
||||
|
||||
// Delegate is notified of key events in the call lifecycle.
|
||||
public weak var delegate: PeerConnectionClientDelegate!
|
||||
private weak var delegate: PeerConnectionClientDelegate!
|
||||
|
||||
func setDelegate(delegate: PeerConnectionClientDelegate?) {
|
||||
PeerConnectionClient.signalingQueue.sync {
|
||||
self.delegate = delegate
|
||||
}
|
||||
}
|
||||
|
||||
// Connection
|
||||
|
||||
internal var peerConnection: RTCPeerConnection!
|
||||
private var peerConnection: RTCPeerConnection!
|
||||
private let iceServers: [RTCIceServer]
|
||||
private let connectionConstraints: RTCMediaConstraints
|
||||
private let configuration: RTCConfiguration
|
||||
|
@ -128,22 +140,26 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
// MARK: - Media Streams
|
||||
|
||||
public func createSignalingDataChannel() {
|
||||
let dataChannel = peerConnection.dataChannel(forLabel: Identifiers.dataChannelSignaling.rawValue,
|
||||
configuration: RTCDataChannelConfiguration())
|
||||
dataChannel.delegate = self
|
||||
PeerConnectionClient.signalingQueue.sync {
|
||||
let dataChannel = peerConnection.dataChannel(forLabel: Identifiers.dataChannelSignaling.rawValue,
|
||||
configuration: RTCDataChannelConfiguration())
|
||||
dataChannel.delegate = self
|
||||
|
||||
assert(self.dataChannel == nil)
|
||||
self.dataChannel = dataChannel
|
||||
assert(self.dataChannel == nil)
|
||||
self.dataChannel = dataChannel
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Video
|
||||
|
||||
fileprivate func createVideoSender() {
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(self.TAG) in \(#function)")
|
||||
assert(self.videoSender == nil, "\(#function) should only be called once.")
|
||||
|
||||
guard !Platform.isSimulator else {
|
||||
Logger.warn("\(TAG) Refusing to create local video track on simulator which has no capture device.")
|
||||
Logger.warn("\(self.TAG) Refusing to create local video track on simulator which has no capture device.")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -169,23 +185,31 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
}
|
||||
|
||||
public func setLocalVideoEnabled(enabled: Bool) {
|
||||
guard let localVideoTrack = self.localVideoTrack else {
|
||||
let action = enabled ? "enable" : "disable"
|
||||
Logger.error("\(TAG)) trying to \(action) videoTrack which doesn't exist")
|
||||
return
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
localVideoTrack.isEnabled = enabled
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
guard let localVideoTrack = self.localVideoTrack else {
|
||||
let action = enabled ? "enable" : "disable"
|
||||
Logger.error("\(self.TAG)) trying to \(action) videoTrack which doesn't exist")
|
||||
return
|
||||
}
|
||||
|
||||
if let delegate = delegate {
|
||||
delegate.peerConnectionClient(self, didUpdateLocal: enabled ? localVideoTrack : nil)
|
||||
localVideoTrack.isEnabled = enabled
|
||||
|
||||
if let delegate = self.delegate {
|
||||
DispatchQueue.main.async {
|
||||
delegate.peerConnectionClient(self, didUpdateLocal: enabled ? localVideoTrack : nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Audio
|
||||
|
||||
fileprivate func createAudioSender() {
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(self.TAG) in \(#function)")
|
||||
assert(self.audioSender == nil, "\(#function) should only be called once.")
|
||||
|
||||
let audioSource = factory.audioSource(with: self.audioConstraints)
|
||||
|
@ -205,18 +229,22 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
}
|
||||
|
||||
public func setAudioEnabled(enabled: Bool) {
|
||||
guard let audioTrack = self.audioTrack else {
|
||||
let action = enabled ? "enable" : "disable"
|
||||
Logger.error("\(TAG) trying to \(action) audioTrack which doesn't exist.")
|
||||
return
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
audioTrack.isEnabled = enabled
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
guard let audioTrack = self.audioTrack else {
|
||||
let action = enabled ? "enable" : "disable"
|
||||
Logger.error("\(self.TAG) trying to \(action) audioTrack which doesn't exist.")
|
||||
return
|
||||
}
|
||||
|
||||
audioTrack.isEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Session negotiation
|
||||
|
||||
var defaultOfferConstraints: RTCMediaConstraints {
|
||||
private var defaultOfferConstraints: RTCMediaConstraints {
|
||||
let mandatoryConstraints = [
|
||||
"OfferToReceiveAudio": "true",
|
||||
"OfferToReceiveVideo": "true"
|
||||
|
@ -224,217 +252,283 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
return RTCMediaConstraints(mandatoryConstraints:mandatoryConstraints, optionalConstraints:nil)
|
||||
}
|
||||
|
||||
func createOffer() -> Promise<HardenedRTCSessionDescription> {
|
||||
return Promise { fulfill, reject in
|
||||
peerConnection.offer(for: self.defaultOfferConstraints, completionHandler: { (sdp: RTCSessionDescription?, error: Error?) in
|
||||
guard error == nil else {
|
||||
reject(error!)
|
||||
return
|
||||
}
|
||||
public func createOffer() -> Promise<HardenedRTCSessionDescription> {
|
||||
var result: Promise<HardenedRTCSessionDescription>? = nil
|
||||
PeerConnectionClient.signalingQueue.sync {
|
||||
result = Promise { fulfill, reject in
|
||||
peerConnection.offer(for: self.defaultOfferConstraints, completionHandler: { (sdp: RTCSessionDescription?, error: Error?) in
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
guard error == nil else {
|
||||
reject(error!)
|
||||
return
|
||||
}
|
||||
|
||||
guard let sessionDescription = sdp else {
|
||||
Logger.error("\(self.TAG) No session description was obtained, even though there was no error reported.")
|
||||
let error = OWSErrorMakeUnableToProcessServerResponseError()
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
guard let sessionDescription = sdp else {
|
||||
Logger.error("\(self.TAG) No session description was obtained, even though there was no error reported.")
|
||||
let error = OWSErrorMakeUnableToProcessServerResponseError()
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
fulfill(HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription))
|
||||
})
|
||||
fulfill(HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// TODO: Propagate exception
|
||||
return result!
|
||||
}
|
||||
|
||||
func setLocalSessionDescription(_ sessionDescription: HardenedRTCSessionDescription) -> Promise<Void> {
|
||||
public func setLocalSessionDescriptionInternal(_ sessionDescription: HardenedRTCSessionDescription) -> Promise<Void> {
|
||||
assertOnSignalingQueue()
|
||||
|
||||
return PromiseKit.wrap {
|
||||
Logger.verbose("\(self.TAG) setting local session description: \(sessionDescription)")
|
||||
peerConnection.setLocalDescription(sessionDescription.rtcSessionDescription, completionHandler: $0)
|
||||
}
|
||||
}
|
||||
|
||||
func negotiateSessionDescription(remoteDescription: RTCSessionDescription, constraints: RTCMediaConstraints) -> Promise<HardenedRTCSessionDescription> {
|
||||
return firstly {
|
||||
return self.setRemoteSessionDescription(remoteDescription)
|
||||
}.then {
|
||||
return self.negotiateAnswerSessionDescription(constraints: constraints)
|
||||
public func setLocalSessionDescription(_ sessionDescription: HardenedRTCSessionDescription) -> Promise<Void> {
|
||||
var result: Promise<Void>? = nil
|
||||
PeerConnectionClient.signalingQueue.sync {
|
||||
result = setLocalSessionDescriptionInternal(sessionDescription)
|
||||
}
|
||||
// TODO: Propagate exception
|
||||
return result!
|
||||
}
|
||||
|
||||
func setRemoteSessionDescription(_ sessionDescription: RTCSessionDescription) -> Promise<Void> {
|
||||
public func negotiateSessionDescription(remoteDescription: RTCSessionDescription, constraints: RTCMediaConstraints) -> Promise<HardenedRTCSessionDescription> {
|
||||
var result: Promise<HardenedRTCSessionDescription>? = nil
|
||||
PeerConnectionClient.signalingQueue.sync {
|
||||
result = firstly {
|
||||
return self.setRemoteSessionDescriptionInternal(remoteDescription)
|
||||
}.then(on: PeerConnectionClient.signalingQueue) {
|
||||
return self.negotiateAnswerSessionDescription(constraints: constraints)
|
||||
}
|
||||
}
|
||||
// TODO: Propagate exception
|
||||
return result!
|
||||
}
|
||||
|
||||
private func setRemoteSessionDescriptionInternal(_ sessionDescription: RTCSessionDescription) -> Promise<Void> {
|
||||
assertOnSignalingQueue()
|
||||
|
||||
return PromiseKit.wrap {
|
||||
Logger.verbose("\(self.TAG) setting remote description: \(sessionDescription)")
|
||||
peerConnection.setRemoteDescription(sessionDescription, completionHandler: $0)
|
||||
}
|
||||
}
|
||||
|
||||
func negotiateAnswerSessionDescription(constraints: RTCMediaConstraints) -> Promise<HardenedRTCSessionDescription> {
|
||||
public func setRemoteSessionDescription(_ sessionDescription: RTCSessionDescription) -> Promise<Void> {
|
||||
var result: Promise<Void>? = nil
|
||||
PeerConnectionClient.signalingQueue.sync {
|
||||
result = setRemoteSessionDescriptionInternal(sessionDescription)
|
||||
}
|
||||
// TODO: Propagate exception
|
||||
return result!
|
||||
}
|
||||
|
||||
private func negotiateAnswerSessionDescription(constraints: RTCMediaConstraints) -> Promise<HardenedRTCSessionDescription> {
|
||||
assertOnSignalingQueue()
|
||||
|
||||
return Promise { fulfill, reject in
|
||||
Logger.debug("\(self.TAG) negotiating answer session.")
|
||||
|
||||
peerConnection.answer(for: constraints, completionHandler: { (sdp: RTCSessionDescription?, error: Error?) in
|
||||
guard error == nil else {
|
||||
reject(error!)
|
||||
return
|
||||
}
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
guard error == nil else {
|
||||
reject(error!)
|
||||
return
|
||||
}
|
||||
|
||||
guard let sessionDescription = sdp else {
|
||||
Logger.error("\(self.TAG) unexpected empty session description, even though no error was reported.")
|
||||
let error = OWSErrorMakeUnableToProcessServerResponseError()
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
guard let sessionDescription = sdp else {
|
||||
Logger.error("\(self.TAG) unexpected empty session description, even though no error was reported.")
|
||||
let error = OWSErrorMakeUnableToProcessServerResponseError()
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
let hardenedSessionDescription = HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription)
|
||||
let hardenedSessionDescription = HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription)
|
||||
|
||||
self.setLocalSessionDescription(hardenedSessionDescription).then {
|
||||
fulfill(hardenedSessionDescription)
|
||||
}.catch { error in
|
||||
reject(error)
|
||||
self.setLocalSessionDescriptionInternal(hardenedSessionDescription)
|
||||
.then(on: PeerConnectionClient.signalingQueue) {
|
||||
fulfill(hardenedSessionDescription)
|
||||
}.catch { error in
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func addIceCandidate(_ candidate: RTCIceCandidate) {
|
||||
Logger.debug("\(TAG) adding candidate")
|
||||
self.peerConnection.add(candidate)
|
||||
public func addIceCandidate(_ candidate: RTCIceCandidate) {
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
Logger.debug("\(self.TAG) adding candidate")
|
||||
self.peerConnection.add(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
func terminate() {
|
||||
// Some notes on preventing crashes while disposing of peerConnection for video calls
|
||||
// from: https://groups.google.com/forum/#!searchin/discuss-webrtc/objc$20crash$20dealloc%7Csort:relevance/discuss-webrtc/7D-vk5yLjn8/rBW2D6EW4GYJ
|
||||
// The sequence to make it work appears to be
|
||||
//
|
||||
// [capturer stop]; // I had to add this as a method to RTCVideoCapturer
|
||||
// [localRenderer stop];
|
||||
// [remoteRenderer stop];
|
||||
// [peerConnection close];
|
||||
public func terminate() {
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
// Some notes on preventing crashes while disposing of peerConnection for video calls
|
||||
// from: https://groups.google.com/forum/#!searchin/discuss-webrtc/objc$20crash$20dealloc%7Csort:relevance/discuss-webrtc/7D-vk5yLjn8/rBW2D6EW4GYJ
|
||||
// The sequence to make it work appears to be
|
||||
//
|
||||
// [capturer stop]; // I had to add this as a method to RTCVideoCapturer
|
||||
// [localRenderer stop];
|
||||
// [remoteRenderer stop];
|
||||
// [peerConnection close];
|
||||
|
||||
// audioTrack is a strong property because we need access to it to mute/unmute, but I was seeing it
|
||||
// become nil when it was only a weak property. So we retain it and manually nil the reference here, because
|
||||
// we are likely to crash if we retain any peer connection properties when the peerconnection is released
|
||||
Logger.debug("\(TAG) in \(#function)")
|
||||
audioTrack = nil
|
||||
localVideoTrack = nil
|
||||
remoteVideoTrack = nil
|
||||
dataChannel = nil
|
||||
audioSender = nil
|
||||
videoSender = nil
|
||||
// audioTrack is a strong property because we need access to it to mute/unmute, but I was seeing it
|
||||
// become nil when it was only a weak property. So we retain it and manually nil the reference here, because
|
||||
// we are likely to crash if we retain any peer connection properties when the peerconnection is released
|
||||
Logger.debug("\(self.TAG) in \(#function)")
|
||||
self.audioTrack = nil
|
||||
self.localVideoTrack = nil
|
||||
self.remoteVideoTrack = nil
|
||||
self.dataChannel = nil
|
||||
self.audioSender = nil
|
||||
self.videoSender = nil
|
||||
|
||||
peerConnection.delegate = nil
|
||||
peerConnection.close()
|
||||
self.peerConnection.delegate = nil
|
||||
self.peerConnection.close()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data Channel
|
||||
|
||||
func sendDataChannelMessage(data: Data) -> Bool {
|
||||
guard let dataChannel = self.dataChannel else {
|
||||
Logger.error("\(TAG) in \(#function) ignoring sending \(data) for nil dataChannel")
|
||||
return false
|
||||
}
|
||||
public func sendDataChannelMessage(data: Data) -> Bool {
|
||||
var result = false
|
||||
PeerConnectionClient.signalingQueue.sync {
|
||||
guard let dataChannel = self.dataChannel else {
|
||||
Logger.error("\(self.TAG) in \(#function) ignoring sending \(data) for nil dataChannel")
|
||||
result = false
|
||||
return
|
||||
}
|
||||
|
||||
let buffer = RTCDataBuffer(data: data, isBinary: false)
|
||||
return dataChannel.sendData(buffer)
|
||||
let buffer = RTCDataBuffer(data: data, isBinary: false)
|
||||
result = dataChannel.sendData(buffer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: RTCDataChannelDelegate
|
||||
|
||||
/** The data channel state changed. */
|
||||
public func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) {
|
||||
Logger.debug("\(TAG) dataChannelDidChangeState: \(dataChannel)")
|
||||
internal func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) {
|
||||
Logger.debug("\(self.TAG) dataChannelDidChangeState: \(dataChannel)")
|
||||
}
|
||||
|
||||
/** The data channel successfully received a data buffer. */
|
||||
public func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) {
|
||||
Logger.debug("\(TAG) dataChannel didReceiveMessageWith buffer:\(buffer)")
|
||||
internal func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) {
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
Logger.debug("\(self.TAG) dataChannel didReceiveMessageWith buffer:\(buffer)")
|
||||
|
||||
guard let dataChannelMessage = OWSWebRTCProtosData.parse(from:buffer.data) else {
|
||||
// TODO can't proto parsings throw an exception? Is it just being lost in the Objc->Swift?
|
||||
Logger.error("\(TAG) failed to parse dataProto")
|
||||
return
|
||||
}
|
||||
guard let dataChannelMessage = OWSWebRTCProtosData.parse(from:buffer.data) else {
|
||||
// TODO can't proto parsings throw an exception? Is it just being lost in the Objc->Swift?
|
||||
Logger.error("\(self.TAG) failed to parse dataProto")
|
||||
return
|
||||
}
|
||||
|
||||
if let delegate = delegate {
|
||||
delegate.peerConnectionClient(self, received: dataChannelMessage)
|
||||
if let delegate = self.delegate {
|
||||
DispatchQueue.main.async {
|
||||
delegate.peerConnectionClient(self, received: dataChannelMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The data channel's |bufferedAmount| changed. */
|
||||
public func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) {
|
||||
Logger.debug("\(TAG) didChangeBufferedAmount: \(amount)")
|
||||
internal func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) {
|
||||
Logger.debug("\(self.TAG) didChangeBufferedAmount: \(amount)")
|
||||
}
|
||||
|
||||
// MARK: - RTCPeerConnectionDelegate
|
||||
|
||||
/** Called when the SignalingState changed. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
|
||||
Logger.debug("\(TAG) didChange signalingState:\(stateChanged.debugDescription)")
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
|
||||
Logger.debug("\(self.TAG) didChange signalingState:\(stateChanged.debugDescription)")
|
||||
}
|
||||
|
||||
/** Called when media is received on a new stream from remote peer. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
|
||||
Logger.debug("\(TAG) didAdd stream:\(stream) video tracks: \(stream.videoTracks.count) audio tracks: \(stream.audioTracks.count)")
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
Logger.debug("\(self.TAG) didAdd stream:\(stream) video tracks: \(stream.videoTracks.count) audio tracks: \(stream.audioTracks.count)")
|
||||
|
||||
if stream.videoTracks.count > 0 {
|
||||
remoteVideoTrack = stream.videoTracks[0]
|
||||
if let delegate = delegate {
|
||||
delegate.peerConnectionClient(self, didUpdateRemote: remoteVideoTrack)
|
||||
if stream.videoTracks.count > 0 {
|
||||
self.remoteVideoTrack = stream.videoTracks[0]
|
||||
if let delegate = self.delegate {
|
||||
let remoteVideoTrack = self.remoteVideoTrack
|
||||
DispatchQueue.main.async {
|
||||
delegate.peerConnectionClient(self, didUpdateRemote: remoteVideoTrack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a remote peer closes a stream. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
|
||||
Logger.debug("\(TAG) didRemove Stream:\(stream)")
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
|
||||
Logger.debug("\(self.TAG) didRemove Stream:\(stream)")
|
||||
}
|
||||
|
||||
/** Called when negotiation is needed, for example ICE has restarted. */
|
||||
public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
|
||||
Logger.debug("\(TAG) shouldNegotiate")
|
||||
internal func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
|
||||
Logger.debug("\(self.TAG) shouldNegotiate")
|
||||
}
|
||||
|
||||
/** Called any time the IceConnectionState changes. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
|
||||
Logger.debug("\(TAG) didChange IceConnectionState:\(newState.debugDescription)")
|
||||
switch newState {
|
||||
case .connected, .completed:
|
||||
if let delegate = delegate {
|
||||
delegate.peerConnectionClientIceConnected(self)
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
Logger.debug("\(self.TAG) didChange IceConnectionState:\(newState.debugDescription)")
|
||||
switch newState {
|
||||
case .connected, .completed:
|
||||
if let delegate = self.delegate {
|
||||
DispatchQueue.main.async {
|
||||
delegate.peerConnectionClientIceConnected(self)
|
||||
}
|
||||
}
|
||||
case .failed:
|
||||
Logger.warn("\(self.TAG) RTCIceConnection failed.")
|
||||
if let delegate = self.delegate {
|
||||
DispatchQueue.main.async {
|
||||
delegate.peerConnectionClientIceFailed(self)
|
||||
}
|
||||
}
|
||||
case .disconnected:
|
||||
Logger.warn("\(self.TAG) RTCIceConnection disconnected.")
|
||||
default:
|
||||
Logger.debug("\(self.TAG) ignoring change IceConnectionState:\(newState.debugDescription)")
|
||||
}
|
||||
case .failed:
|
||||
Logger.warn("\(self.TAG) RTCIceConnection failed.")
|
||||
if let delegate = delegate {
|
||||
delegate.peerConnectionClientIceFailed(self)
|
||||
}
|
||||
case .disconnected:
|
||||
Logger.warn("\(self.TAG) RTCIceConnection disconnected.")
|
||||
default:
|
||||
Logger.debug("\(self.TAG) ignoring change IceConnectionState:\(newState.debugDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/** Called any time the IceGatheringState changes. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
|
||||
Logger.debug("\(TAG) didChange IceGatheringState:\(newState.debugDescription)")
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
|
||||
Logger.debug("\(self.TAG) didChange IceGatheringState:\(newState.debugDescription)")
|
||||
}
|
||||
|
||||
/** New ice candidate has been found. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
|
||||
Logger.debug("\(TAG) didGenerate IceCandidate:\(candidate.sdp)")
|
||||
if let delegate = delegate {
|
||||
delegate.peerConnectionClient(self, addedLocalIceCandidate: candidate)
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
Logger.debug("\(self.TAG) didGenerate IceCandidate:\(candidate.sdp)")
|
||||
if let delegate = self.delegate {
|
||||
DispatchQueue.main.async {
|
||||
delegate.peerConnectionClient(self, addedLocalIceCandidate: candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a group of local Ice candidates have been removed. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
|
||||
Logger.debug("\(TAG) didRemove IceCandidates:\(candidates)")
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
|
||||
Logger.debug("\(self.TAG) didRemove IceCandidates:\(candidates)")
|
||||
}
|
||||
|
||||
/** New data channel has been opened. */
|
||||
public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
|
||||
Logger.debug("\(TAG) didOpen dataChannel:\(dataChannel)")
|
||||
CallService.signalingQueue.async {
|
||||
Logger.debug("\(self.TAG) set dataChannel")
|
||||
internal func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
|
||||
PeerConnectionClient.signalingQueue.async {
|
||||
Logger.debug("\(self.TAG) didOpen dataChannel:\(dataChannel)")
|
||||
assert(self.dataChannel == nil)
|
||||
self.dataChannel = dataChannel
|
||||
dataChannel.delegate = self
|
||||
|
@ -455,6 +549,18 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
sharedAudioSession.stop()
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
/**
|
||||
* We synchronize access to state in this class using this queue.
|
||||
*/
|
||||
private func assertOnSignalingQueue() {
|
||||
if #available(iOS 10.0, *) {
|
||||
dispatchPrecondition(condition: .onQueue(type(of: self).signalingQueue))
|
||||
} else {
|
||||
// Skipping check on <iOS10, since syntax is different and it's just a development convenience.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,7 +28,7 @@ protocol CallObserver: class {
|
|||
/**
|
||||
* Data model for a WebRTC backed voice/video call.
|
||||
*
|
||||
* This class' state should only be accessed on the signaling queue.
|
||||
* This class' state should only be accessed on the main queue.
|
||||
*/
|
||||
@objc class SignalCall: NSObject {
|
||||
|
||||
|
@ -45,27 +45,17 @@ protocol CallObserver: class {
|
|||
|
||||
var hasLocalVideo = false {
|
||||
didSet {
|
||||
// This should only occur on the signaling queue.
|
||||
objc_sync_enter(self)
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let observers = self.observers
|
||||
let call = self
|
||||
let hasLocalVideo = self.hasLocalVideo
|
||||
|
||||
objc_sync_exit(self)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
for observer in observers {
|
||||
observer.value?.hasLocalVideoDidChange(call: call, hasLocalVideo: hasLocalVideo)
|
||||
}
|
||||
for observer in observers {
|
||||
observer.value?.hasLocalVideoDidChange(call: self, hasLocalVideo: hasLocalVideo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var state: CallState {
|
||||
didSet {
|
||||
// This should only occur on the signaling queue.
|
||||
objc_sync_enter(self)
|
||||
AssertIsOnMainThread()
|
||||
Logger.debug("\(TAG) state changed: \(oldValue) -> \(self.state)")
|
||||
|
||||
// Update connectedDate
|
||||
|
@ -77,58 +67,32 @@ protocol CallObserver: class {
|
|||
connectedDate = nil
|
||||
}
|
||||
|
||||
let observers = self.observers
|
||||
let call = self
|
||||
let state = self.state
|
||||
|
||||
objc_sync_exit(self)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
for observer in observers {
|
||||
observer.value?.stateDidChange(call: call, state: state)
|
||||
}
|
||||
for observer in observers {
|
||||
observer.value?.stateDidChange(call: self, state: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isMuted = false {
|
||||
didSet {
|
||||
// This should only occur on the signaling queue.
|
||||
objc_sync_enter(self)
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(TAG) muted changed: \(oldValue) -> \(self.isMuted)")
|
||||
|
||||
let observers = self.observers
|
||||
let call = self
|
||||
let isMuted = self.isMuted
|
||||
|
||||
objc_sync_exit(self)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
for observer in observers {
|
||||
observer.value?.muteDidChange(call: call, isMuted: isMuted)
|
||||
}
|
||||
for observer in observers {
|
||||
observer.value?.muteDidChange(call: self, isMuted: isMuted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isSpeakerphoneEnabled = false {
|
||||
didSet {
|
||||
// This should only occur on the signaling queue.
|
||||
objc_sync_enter(self)
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(TAG) isSpeakerphoneEnabled changed: \(oldValue) -> \(self.isSpeakerphoneEnabled)")
|
||||
|
||||
let observers = self.observers
|
||||
let call = self
|
||||
let isSpeakerphoneEnabled = self.isSpeakerphoneEnabled
|
||||
|
||||
objc_sync_exit(self)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
for observer in observers {
|
||||
observer.value?.speakerphoneDidChange(call: call, isEnabled: isSpeakerphoneEnabled)
|
||||
}
|
||||
for observer in observers {
|
||||
observer.value?.speakerphoneDidChange(call: self, isEnabled: isSpeakerphoneEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -159,37 +123,26 @@ protocol CallObserver: class {
|
|||
// -
|
||||
|
||||
func addObserverAndSyncState(observer: CallObserver) {
|
||||
objc_sync_enter(self)
|
||||
AssertIsOnMainThread()
|
||||
|
||||
observers.append(Weak(value: observer))
|
||||
|
||||
let call = self
|
||||
let state = self.state
|
||||
|
||||
objc_sync_exit(self)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Synchronize observer with current call state
|
||||
observer.stateDidChange(call: call, state: state)
|
||||
}
|
||||
// Synchronize observer with current call state
|
||||
observer.stateDidChange(call: self, state: state)
|
||||
}
|
||||
|
||||
func removeObserver(_ observer: CallObserver) {
|
||||
objc_sync_enter(self)
|
||||
AssertIsOnMainThread()
|
||||
|
||||
while let index = observers.index(where: { $0.value === observer }) {
|
||||
observers.remove(at: index)
|
||||
}
|
||||
|
||||
objc_sync_exit(self)
|
||||
}
|
||||
|
||||
func removeAllObservers() {
|
||||
objc_sync_enter(self)
|
||||
AssertIsOnMainThread()
|
||||
|
||||
observers = []
|
||||
|
||||
objc_sync_exit(self)
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
|
|
@ -171,9 +171,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
|||
// Update the CallKit UI.
|
||||
provider.reportCall(with: call.localId, updated: update)
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
self.callService.setHasLocalVideo(hasLocalVideo: hasLocalVideo)
|
||||
}
|
||||
self.callService.setHasLocalVideo(hasLocalVideo: hasLocalVideo)
|
||||
}
|
||||
|
||||
// MARK: CXProviderDelegate
|
||||
|
@ -213,15 +211,13 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
self.callService.handleOutgoingCall(call).then { () -> Void in
|
||||
action.fulfill()
|
||||
self.provider.reportOutgoingCall(with: call.localId, startedConnectingAt: nil)
|
||||
self.callService.handleOutgoingCall(call).then { () -> Void in
|
||||
action.fulfill()
|
||||
self.provider.reportOutgoingCall(with: call.localId, startedConnectingAt: nil)
|
||||
}.catch { error in
|
||||
Logger.error("\(self.TAG) error \(error) in \(#function)")
|
||||
self.callManager.removeCall(call)
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,11 +241,9 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
|||
// // Trigger the call to be answered via the underlying network service.
|
||||
// call.answerSpeakerboxCall()
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
self.callService.handleAnswerCall(call)
|
||||
self.showCall(call)
|
||||
action.fulfill()
|
||||
}
|
||||
self.callService.handleAnswerCall(call)
|
||||
self.showCall(call)
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
|
@ -268,15 +262,15 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
|||
// call.endSpeakerboxCall()
|
||||
|
||||
// Synchronous to ensure call is terminated before call is displayed as "ended"
|
||||
CallService.signalingQueue.sync {
|
||||
self.callService.handleLocalHungupCall(call)
|
||||
self.callService.handleLocalHungupCall(call)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Signal to the system that the action has been successfully performed.
|
||||
action.fulfill()
|
||||
|
||||
// Remove the ended call from the app's list of calls.
|
||||
self.callManager.removeCall(call)
|
||||
}
|
||||
|
||||
// Signal to the system that the action has been successfully performed.
|
||||
action.fulfill()
|
||||
|
||||
// Remove the ended call from the app's list of calls.
|
||||
callManager.removeCall(call)
|
||||
}
|
||||
|
||||
public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
|
||||
|
@ -315,10 +309,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
self.callService.setIsMuted(isMuted: action.isMuted)
|
||||
action.fulfill()
|
||||
}
|
||||
self.callService.setIsMuted(isMuted: action.isMuted)
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
public func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
|
||||
|
|
|
@ -30,24 +30,29 @@ protocol CallUIAdaptee {
|
|||
// Shared default implementations
|
||||
extension CallUIAdaptee {
|
||||
internal func showCall(_ call: SignalCall) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let callNotificationName = CallService.callServiceActiveCallNotificationName()
|
||||
NotificationCenter.default.post(name: NSNotification.Name(rawValue: callNotificationName), object: call)
|
||||
}
|
||||
|
||||
internal func reportMissedCall(_ call: SignalCall, callerName: String) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
notificationsAdapter.presentMissedCall(call, callerName: callerName)
|
||||
}
|
||||
|
||||
// TODO: who calls this?
|
||||
internal func callBack(recipientId: String) {
|
||||
CallService.signalingQueue.async {
|
||||
guard self.callService.call == nil else {
|
||||
assertionFailure("unexpectedly found an existing call when trying to call back: \(recipientId)")
|
||||
return
|
||||
}
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let call = self.startOutgoingCall(handle: recipientId)
|
||||
self.showCall(call)
|
||||
guard self.callService.call == nil else {
|
||||
assertionFailure("unexpectedly found an existing call when trying to call back: \(recipientId)")
|
||||
return
|
||||
}
|
||||
|
||||
let call = self.startOutgoingCall(handle: recipientId)
|
||||
self.showCall(call)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,9 +192,7 @@ extension CallUIAdaptee {
|
|||
// Speakerphone is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the
|
||||
// adaptee, relying on the AudioService CallObserver to put the system in a state consistent with the call's
|
||||
// assigned property.
|
||||
CallService.signalingQueue.async {
|
||||
call.isSpeakerphoneEnabled = isEnabled
|
||||
}
|
||||
call.isSpeakerphoneEnabled = isEnabled
|
||||
}
|
||||
|
||||
// CallKit handles ringing state on it's own. But for non-call kit we trigger ringing start/stop manually.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Created by Michael Kirk on 12/4/16.
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
@ -30,7 +31,7 @@ class WebRTCCallMessageHandler: NSObject, OWSCallMessageHandler {
|
|||
Logger.verbose("\(TAG) handling offer from caller:\(callerId)")
|
||||
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
CallService.signalingQueue.async {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.callService.handleReceivedOffer(thread: thread, callId: offer.id, sessionDescription: offer.sessionDescription)
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +40,7 @@ class WebRTCCallMessageHandler: NSObject, OWSCallMessageHandler {
|
|||
Logger.verbose("\(TAG) handling answer from caller:\(callerId)")
|
||||
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
CallService.signalingQueue.async {
|
||||
DispatchQueue.main.async {
|
||||
self.callService.handleReceivedAnswer(thread: thread, callId: answer.id, sessionDescription: answer.sessionDescription)
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +54,7 @@ class WebRTCCallMessageHandler: NSObject, OWSCallMessageHandler {
|
|||
// while the RTC iOS API requires a signed int.
|
||||
let lineIndex = Int32(iceUpdate.sdpMlineIndex)
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
DispatchQueue.main.async {
|
||||
self.callService.handleRemoteAddedIceCandidate(thread: thread, callId: iceUpdate.id, sdp: iceUpdate.sdp, lineIndex: lineIndex, mid: iceUpdate.sdpMid)
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +64,7 @@ class WebRTCCallMessageHandler: NSObject, OWSCallMessageHandler {
|
|||
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
DispatchQueue.main.async {
|
||||
self.callService.handleRemoteHangup(thread: thread)
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +74,7 @@ class WebRTCCallMessageHandler: NSObject, OWSCallMessageHandler {
|
|||
|
||||
let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
|
||||
CallService.signalingQueue.async {
|
||||
DispatchQueue.main.async {
|
||||
self.callService.handleRemoteBusy(thread: thread)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue