//
// C o p y r i g h t ( c ) 2 0 1 7 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import PromiseKit
import WebRTC
/* *
* ` CallService ` is a global singleton that manages the state of WebRTC - backed Signal Calls
* ( as opposed to legacy " RedPhone Calls " ) .
*
* It serves as a connection between the ` CallUIAdapter ` and the ` PeerConnectionClient ` .
*
* # # 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
* ( like hangup ) via the established channel .
*
* 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 .
*
* # # # Key
*
* -- [ SOMETHING ] --> represents a message of type " Something " sent from the caller to the callee
* <-- [ SOMETHING ] -- represents a message of type " Something " sent from the callee to the caller
* SS : Message sent via Signal Service
* DC : Message sent via WebRTC Data Channel
*
* # # # Message Exchange / State Flow Overview
*
* | Caller | Callee |
* +----------------------------+-------------------------+
* Start outgoing call : ` handleOutgoingCall ` . . .
-- [ SS . CallOffer ] -->
* . . . and start generating ICE updates .
* As ICE candidates are generated , ` handleLocalAddedIceCandidate ` is called .
* and we * store * the ICE updates for later .
*
* Received call offer : ` handleReceivedOffer `
* Send call answer
* <-- [ SS . CallAnswer ] --
* Start generating ICE updates .
* As they are generated ` handleLocalAddedIceCandidate ` is called
which immediately sends the ICE updates to the Caller .
* <-- [ SS . ICEUpdate ] -- ( sent multiple times )
*
* Received CallAnswer : ` handleReceivedAnswer `
* So send any stored ice updates ( and send future ones immediately )
* -- [ SS . ICEUpdates ] -->
*
* Once compatible ICE updates have been exchanged . . .
* both parties : ` handleIceConnected `
*
* Show remote ringing UI
* Connect to offered Data Channel
* Show incoming call UI .
*
* If callee answers Call
* send connected message
* <-- [ DC . ConnectedMesage ] --
* Received connected message
* Show Call is connected .
*
* Hang up ( this could equally be sent by the Callee )
* -- [ DC . Hangup ] -->
* -- [ SS . Hangup ] -->
*/
enum CallError : Error {
case providerReset
case assertionError ( description : String )
case disconnected
case externalError ( underlyingError : Error )
case timeout ( description : String )
}
// F I X M E T O D O d o w e n e e d t o t i m e o u t ?
fileprivate let timeoutSeconds = 60
// A l l O b s e r v e r m e t h o d s w i l l b e i n v o k e d f r o m t h e m a i n t h r e a d .
protocol CallServiceObserver : class {
/* *
* Fired whenever the local or remote video track become active or inactive .
*/
func didUpdateVideoTracks ( localVideoTrack : RTCVideoTrack ? ,
remoteVideoTrack : RTCVideoTrack ? )
}
// T h i s c l a s s ' s t a t e s h o u l d o n l y b e a c c e s s e d o n t h e m a i n q u e u e .
@objc class CallService : NSObject , CallObserver , PeerConnectionClientDelegate {
// MARK: - P r o p e r t i e s
let TAG = " [CallService] "
var observers = [ Weak < CallServiceObserver > ] ( )
// MARK: D e p e n d e n c i e s
private let accountManager : AccountManager
private let messageSender : MessageSender
private let contactsManager : OWSContactsManager
private let notificationsAdapter : CallNotificationsAdapter
// E x p o s e d b y e n v i r o n m e n t . m
internal var callUIAdapter : CallUIAdapter !
// MARK: C l a s s
static let fallbackIceServer = RTCIceServer ( urlStrings : [ " stun:stun1.l.google.com:19302 " ] )
// MARK: I v a r s
var peerConnectionClient : PeerConnectionClient ? {
didSet {
AssertIsOnMainThread ( )
Logger . debug ( " \( self . TAG ) .peerConnectionClient setter: \( oldValue != nil ) -> \( peerConnectionClient != nil ) " )
}
}
// T O D O c o d e c l e a n u p : m o v e t h r e a d i n t o S i g n a l C a l l ? O r r e f a c t o r m e s s a g e S e n d e r t o t a k e S i g n a l R e c i p i e n t i d e n t i f i e r .
var thread : TSContactThread ?
var call : SignalCall ? {
didSet {
AssertIsOnMainThread ( )
oldValue ? . removeObserver ( self )
call ? . addObserverAndSyncState ( observer : self )
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
* 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 `
* message .
*/
var sendIceUpdatesImmediately = true
var pendingIceUpdateMessages = [ OWSCallIceUpdateMessage ] ( )
// e n s u r e t h e i n c o m i n g c a l l p r o m i s e i s n ' t d e a l l o c ' d p r e m a t u r e l y
var incomingCallPromise : Promise < Void > ?
// U s e d t o c o o r d i n a t e p r o m i s e s a c r o s s d e l e g a t e m e t h o d s
var fulfillCallConnectedPromise : ( ( ) -> Void ) ?
weak var localVideoTrack : RTCVideoTrack ? {
didSet {
AssertIsOnMainThread ( )
Logger . info ( " \( self . TAG ) \( #function ) " )
fireDidUpdateVideoTracks ( )
}
}
weak var remoteVideoTrack : RTCVideoTrack ? {
didSet {
AssertIsOnMainThread ( )
Logger . info ( " \( self . TAG ) \( #function ) " )
fireDidUpdateVideoTracks ( )
}
}
var isRemoteVideoEnabled = false {
didSet {
AssertIsOnMainThread ( )
Logger . info ( " \( self . TAG ) \( #function ) " )
fireDidUpdateVideoTracks ( )
}
}
required init ( accountManager : AccountManager , contactsManager : OWSContactsManager , messageSender : MessageSender , notificationsAdapter : CallNotificationsAdapter ) {
self . accountManager = accountManager
self . contactsManager = contactsManager
self . messageSender = messageSender
self . notificationsAdapter = notificationsAdapter
super . init ( )
self . createCallUIAdapter ( )
NotificationCenter . default . addObserver ( self ,
selector : #selector ( didEnterBackground ) ,
name : NSNotification . Name . UIApplicationDidEnterBackground ,
object : nil )
NotificationCenter . default . addObserver ( self ,
selector : #selector ( didBecomeActive ) ,
name : NSNotification . Name . UIApplicationDidBecomeActive ,
object : nil )
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
func didEnterBackground ( ) {
AssertIsOnMainThread ( )
Logger . info ( " \( self . TAG ) \( #function ) " )
self . updateIsVideoEnabled ( )
}
func didBecomeActive ( ) {
AssertIsOnMainThread ( )
Logger . info ( " \( self . TAG ) \( #function ) " )
self . updateIsVideoEnabled ( )
}
public func createCallUIAdapter ( ) {
AssertIsOnMainThread ( )
if self . call != nil {
Logger . warn ( " \( TAG ) ending current call in \( #function ) . Did user toggle callkit preference while in a call? " )
self . terminateCall ( )
}
self . callUIAdapter = CallUIAdapter ( callService : self , contactsManager : self . contactsManager , notificationsAdapter : self . notificationsAdapter )
}
// MARK: - C l a s s M e t h o d s
// MARK: N o t i f i c a t i o n s
// W r a p p i n g t h e s e c l a s s c o n s t a n t s i n a m e t h o d t o m a k e i t a c c e s s i b l e t o o b j c
class func callServiceActiveCallNotificationName ( ) -> String {
return " CallServiceActiveCallNotification "
}
// MARK: - S e r v i c e A c t i o n s
/* *
* Initiate an outgoing call .
*/
public func handleOutgoingCall ( _ call : SignalCall ) -> Promise < Void > {
AssertIsOnMainThread ( )
guard self . call = = nil else {
let errorDescription = " \( TAG ) call was unexpectedly already set. "
Logger . error ( errorDescription )
call . state = . localFailure
return Promise ( error : CallError . assertionError ( description : errorDescription ) )
}
self . call = call
let thread = TSContactThread . getOrCreateThread ( contactId : call . remotePhoneNumber )
self . thread = thread
sendIceUpdatesImmediately = false
pendingIceUpdateMessages = [ ]
let callRecord = TSCall ( timestamp : NSDate . ows_millisecondTimeStamp ( ) , withCallNumber : call . remotePhoneNumber , callType : RPRecentCallTypeOutgoing , in : thread )
callRecord . save ( )
guard self . peerConnectionClient = = nil else {
let errorDescription = " \( TAG ) peerconnection was unexpectedly already set. "
Logger . error ( errorDescription )
call . state = . localFailure
return Promise ( error : CallError . assertionError ( description : errorDescription ) )
}
return getIceServers ( ) . then { iceServers -> Promise < HardenedRTCSessionDescription > in
Logger . debug ( " \( self . TAG ) got ice servers: \( iceServers ) " )
let peerConnectionClient = PeerConnectionClient ( iceServers : iceServers , delegate : self )
// W h e n p l a c i n g a n o u t g o i n g c a l l , i t ' s o u r r e s p o n s i b i l i t y t o c r e a t e t h e D a t a C h a n n e l . R e c i p i e n t w i l l n o t h a v e
// t o d o t h i s e x p l i c i t l y .
peerConnectionClient . createSignalingDataChannel ( )
assert ( self . peerConnectionClient = = nil , " Unexpected PeerConnectionClient instance " )
Logger . debug ( " \( self . TAG ) setting peerConnectionClient in \( #function ) " )
self . peerConnectionClient = peerConnectionClient
return self . peerConnectionClient ! . createOffer ( )
} . then { ( sessionDescription : HardenedRTCSessionDescription ) -> Promise < Void > in
return self . peerConnectionClient ! . setLocalSessionDescription ( sessionDescription ) . then {
let offerMessage = OWSCallOfferMessage ( callId : call . signalingId , sessionDescription : sessionDescription . sdp )
let callMessage = OWSOutgoingCallMessage ( thread : thread , offerMessage : offerMessage )
return self . messageSender . sendCallMessage ( callMessage )
}
} . catch { 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 )
}
}
}
/* *
* Called by the call initiator after receiving a CallAnswer from the callee .
*/
public func handleReceivedAnswer ( thread : TSContactThread , callId : UInt64 , sessionDescription : String ) {
Logger . debug ( " \( TAG ) received call answer for call: \( callId ) thread: \( thread ) " )
AssertIsOnMainThread ( )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " call was unexpectedly nil in \( #function ) " ) )
return
}
guard call . signalingId = = callId else {
let description : String = " received answer for call: \( callId ) but current call has id: \( call . signalingId ) "
handleFailedCall ( error : . assertionError ( description : description ) )
return
}
// N o w t h a t w e k n o w t h e r e c i p i e n t t r u s t s o u r i d e n t i t y , w e n o l o n g e r n e e d t o e n q u e u e I C E u p d a t e s .
self . sendIceUpdatesImmediately = true
if pendingIceUpdateMessages . count > 0 {
let callMessage = OWSOutgoingCallMessage ( thread : thread , iceUpdateMessages : pendingIceUpdateMessages )
_ = messageSender . sendCallMessage ( callMessage ) . catch { error in
Logger . error ( " \( self . TAG ) failed to send ice updates in \( #function ) with error: \( error ) " )
}
}
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : CallError . assertionError ( description : " peerConnectionClient was unexpectedly nil in \( #function ) " ) )
return
}
let sessionDescription = RTCSessionDescription ( type : . answer , sdp : sessionDescription )
_ = peerConnectionClient . setRemoteSessionDescription ( sessionDescription ) . then {
Logger . debug ( " \( self . TAG ) successfully set remote description " )
} . catch { error in
if let callError = error as ? CallError {
self . handleFailedCall ( error : callError )
} else {
let externalError = CallError . externalError ( underlyingError : error )
self . handleFailedCall ( error : externalError )
}
}
}
/* *
* User didn ' t answer incoming call
*/
public func handleMissedCall ( _ call : SignalCall , thread : TSContactThread ) {
AssertIsOnMainThread ( )
// I n s e r t m i s s e d c a l l r e c o r d
let callRecord = TSCall ( timestamp : NSDate . ows_millisecondTimeStamp ( ) ,
withCallNumber : thread . contactIdentifier ( ) ,
callType : RPRecentCallTypeMissed ,
in : thread )
callRecord . save ( )
self . callUIAdapter . reportMissedCall ( call )
}
/* *
* Received a call while already in another call .
*/
private func handleLocalBusyCall ( _ call : SignalCall , thread : TSContactThread ) {
Logger . debug ( " \( TAG ) \( #function ) for call: \( call ) thread: \( thread ) " )
AssertIsOnMainThread ( )
let busyMessage = OWSCallBusyMessage ( callId : call . signalingId )
let callMessage = OWSOutgoingCallMessage ( thread : thread , busyMessage : busyMessage )
_ = messageSender . sendCallMessage ( callMessage )
handleMissedCall ( call , thread : thread )
}
/* *
* The callee was already in another call .
*/
public func handleRemoteBusy ( thread : TSContactThread ) {
Logger . debug ( " \( TAG ) \( #function ) for thread: \( thread ) " )
AssertIsOnMainThread ( )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " call unexpectedly nil in \( #function ) " ) )
return
}
call . state = . remoteBusy
callUIAdapter . remoteBusy ( call )
terminateCall ( )
}
/* *
* Received an incoming call offer . We still have to complete setting up the Signaling channel before we notify
* the user of an incoming call .
*/
public func handleReceivedOffer ( thread : TSContactThread , callId : UInt64 , sessionDescription callerSessionDescription : String ) {
AssertIsOnMainThread ( )
Logger . verbose ( " \( TAG ) receivedCallOffer for thread: \( thread ) " )
let newCall = SignalCall . incomingCall ( localId : UUID ( ) , remotePhoneNumber : thread . contactIdentifier ( ) , signalingId : callId )
guard call = = nil || Environment . getCurrent ( ) . phoneManager . hasOngoingCall ( ) else {
// T O D O o n i O S 1 0 + w e c a n u s e C a l l K i t t o s w a p c a l l s r a t h e r t h a n j u s t r e t u r n i n g b u s y i m m e d i a t e l y .
Logger . verbose ( " \( TAG ) receivedCallOffer for thread: \( thread ) but we're already in call: \( call ) " )
handleLocalBusyCall ( newCall , thread : thread )
return
}
self . thread = thread
call = newCall
let backgroundTask = UIApplication . shared . beginBackgroundTask {
let timeout = CallError . timeout ( description : " background task time ran out before call connected. " )
DispatchQueue . main . async {
self . handleFailedCall ( error : timeout )
}
}
incomingCallPromise = firstly {
return getIceServers ( )
} . then { ( iceServers : [ RTCIceServer ] ) -> Promise < HardenedRTCSessionDescription > in
// F I X M E f o r f i r s t t i m e c a l l r e c i p i e n t s I t h i n k w e ' l l s e e m i c / c a m e r a p e r m i s s i o n r e q u e s t s h e r e ,
// e v e n t h o u g h , f r o m t h e u s e r s p e r s p e c t i v e , n o i n c o m i n g c a l l i s y e t v i s i b l e .
assert ( self . peerConnectionClient = = nil , " Unexpected PeerConnectionClient instance " )
Logger . debug ( " \( self . self . TAG ) setting peerConnectionClient in \( #function ) " )
self . peerConnectionClient = PeerConnectionClient ( iceServers : iceServers , delegate : self )
let offerSessionDescription = RTCSessionDescription ( type : . offer , sdp : callerSessionDescription )
let constraints = RTCMediaConstraints ( mandatoryConstraints : nil , optionalConstraints : nil )
// F i n d a s e s s i o n D e s c r i p t i o n c o m p a t i b l e w i t h m y c o n s t r a i n t s a n d t h e r e m o t e s e s s i o n D e s c r i p t i o n
return self . peerConnectionClient ! . negotiateSessionDescription ( remoteDescription : offerSessionDescription , constraints : constraints )
} . then { ( 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 )
return self . messageSender . sendCallMessage ( callAnswerMessage )
} . then {
Logger . debug ( " \( self . TAG ) successfully sent callAnswerMessage " )
let ( promise , fulfill , _ ) = Promise < Void > . pending ( )
let timeout : Promise < Void > = after ( interval : TimeInterval ( timeoutSeconds ) ) . then { ( ) -> Void in
// r e j e c t i n g a p r o m i s e b y t h r o w i n g i s s a f e l y a n o - o p i f t h e p r o m i s e h a s a l r e a d y b e e n f u l f i l l e d
throw CallError . timeout ( description : " timed out waiting for call to connect " )
}
// T h i s w i l l b e f u l f i l l e d ( p o t e n t i a l l y ) b y t h e R T C D a t a C h a n n e l d e l e g a t e m e t h o d
self . fulfillCallConnectedPromise = fulfill
return race ( promise , timeout )
} . catch { 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 )
}
}
/* *
* 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 ) {
AssertIsOnMainThread ( )
Logger . debug ( " \( TAG ) called \( #function ) " )
guard self . thread != nil else {
handleFailedCall ( error : . assertionError ( description : " ignoring remote ice update for thread: \( thread . uniqueId ) since there is no current thread. Call already ended? " ) )
return
}
guard thread . contactIdentifier ( ) = = self . thread ! . contactIdentifier ( ) else {
handleFailedCall ( error : . assertionError ( description : " ignoring remote ice update for thread: \( thread . uniqueId ) since the current call is for thread: \( self . thread ! . uniqueId ) " ) )
return
}
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " ignoring remote ice update for callId: \( callId ) , since there is no current call. " ) )
return
}
guard call . signalingId = = callId else {
handleFailedCall ( error : . assertionError ( description : " ignoring remote ice update for call: \( callId ) since the current call is: \( call . signalingId ) " ) )
return
}
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " ignoring remote ice update for thread: \( thread ) since the current call hasn't initialized it's peerConnectionClient " ) )
return
}
peerConnectionClient . addIceCandidate ( RTCIceCandidate ( sdp : sdp , sdpMLineIndex : lineIndex , sdpMid : mid ) )
}
/* *
* Local client ( could be caller or callee ) generated some connectivity information that we should send to the
* remote client .
*/
private func handleLocalAddedIceCandidate ( _ iceCandidate : RTCIceCandidate ) {
AssertIsOnMainThread ( )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " ignoring local ice candidate, since there is no current call. " ) )
return
}
guard call . state != . idle else {
handleFailedCall ( error : . assertionError ( description : " ignoring local ice candidate, since call is now idle. " ) )
return
}
guard let thread = self . thread else {
handleFailedCall ( error : . assertionError ( description : " ignoring local ice candidate, because there was no current TSContactThread. " ) )
return
}
let iceUpdateMessage = OWSCallIceUpdateMessage ( callId : call . signalingId , sdp : iceCandidate . sdp , sdpMLineIndex : iceCandidate . sdpMLineIndex , sdpMid : iceCandidate . sdpMid )
if self . sendIceUpdatesImmediately {
let callMessage = OWSOutgoingCallMessage ( thread : thread , iceUpdateMessage : iceUpdateMessage )
_ = self . messageSender . sendCallMessage ( callMessage )
} else {
// F o r o u t g o i n g m e s s a g e s , w e w a i t t o s e n d i c e u p d a t e s u n t i l w e ' r e s u r e c l i e n t r e c e i v e d o u r c a l l m e s s a g e .
// e . g . i f t h e c l i e n t h a s b l o c k e d o u r m e s s a g e d u e t o a n i d e n t i t y c h a n g e , w e ' d o t h e r w i s e
// b o m b a r d t h e m w i t h a b u n c h * m o r e * u n d e c i p h e r a b l e m e s s a g e s .
Logger . debug ( " \( TAG ) enqueuing iceUpdate until we receive call answer " )
self . pendingIceUpdateMessages . append ( iceUpdateMessage )
return
}
}
/* *
* The clients can now communicate via WebRTC .
*
* Called by both caller and callee . Compatible ICE messages have been exchanged between the local and remote
* client .
*/
private func handleIceConnected ( ) {
AssertIsOnMainThread ( )
Logger . debug ( " \( TAG ) in \( #function ) " )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) ignoring \( #function ) since there is no current call. " ) )
return
}
guard let thread = self . thread else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) ignoring \( #function ) since there is no current thread. " ) )
return
}
switch call . state {
case . dialing :
call . state = . remoteRinging
case . answering :
call . state = . localRinging
self . callUIAdapter . reportIncomingCall ( call , thread : thread )
// c a n c e l c o n n e c t i o n t i m e o u t
self . fulfillCallConnectedPromise ? ( )
case . remoteRinging :
Logger . info ( " \( TAG ) call alreading ringing. Ignoring \( #function ) " )
case . connected :
Logger . info ( " \( TAG ) Call reconnected \( #function ) " )
default :
Logger . debug ( " \( TAG ) unexpected call state for \( #function ) : \( call . state ) " )
}
}
/* *
* The remote client ( caller or callee ) ended the call .
*/
public func handleRemoteHangup ( thread : TSContactThread ) {
Logger . debug ( " \( TAG ) in \( #function ) " )
AssertIsOnMainThread ( )
guard thread . contactIdentifier ( ) = = self . thread ? . contactIdentifier ( ) else {
// T h i s c a n s a f e l y b e i g n o r e d .
// W e d o n ' t w a n t t o f a i l t h e c u r r e n t c a l l b e c a u s e a n o l d c a l l w a s s l o w t o s e n d u s t h e h a n g u p m e s s a g e .
Logger . warn ( " \( TAG ) ignoring hangup for thread: \( thread ) which is not the current thread: \( self . thread ) " )
return
}
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) call was unexpectedly nil in \( #function ) " ) )
return
}
switch call . state {
case . idle , . dialing , . answering , . localRinging , . localFailure , . remoteBusy , . remoteRinging :
handleMissedCall ( call , thread : thread )
case . connected , . localHangup , . remoteHangup :
Logger . info ( " \( TAG ) call is finished. " )
}
call . state = . remoteHangup
// N o t i f y U I
callUIAdapter . remoteDidHangupCall ( call )
// s e l f . c a l l i s n i l ' d i n ` t e r m i n a t e C a l l ` , s o i t ' s i m p o r t a n t w e u p d a t e i t ' s s t a t e * b e f o r e * c a l l i n g ` t e r m i n a t e C a l l `
terminateCall ( )
}
/* *
* User chose to answer call referrred to by call ` localId ` . Used by the Callee only .
*
* Used by notification actions which can ' t serialize a call object .
*/
public func handleAnswerCall ( localId : UUID ) {
AssertIsOnMainThread ( )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) call was unexpectedly nil in \( #function ) " ) )
return
}
guard call . localId = = localId else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) callLocalId: \( localId ) doesn't match current calls: \( call . localId ) " ) )
return
}
self . handleAnswerCall ( call )
}
/* *
* User chose to answer call referrred to by call ` localId ` . Used by the Callee only .
*/
public func handleAnswerCall ( _ call : SignalCall ) {
AssertIsOnMainThread ( )
Logger . debug ( " \( TAG ) in \( #function ) " )
guard self . call != nil else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) ignoring \( #function ) since there is no current call " ) )
return
}
guard call = = self . call ! else {
// T h i s c o u l d c o n c e i v a b l y h a p p e n i f t h e o t h e r p a r t y o f a n o l d c a l l w a s s l o w t o s e n d u s t h e i r a n s w e r
// a n d w e ' v e s u b s e q u e n t l y e n g a g e d i n a n o t h e r c a l l . D o n ' t k i l l t h e c u r r e n t c a l l , b u t j u s t i g n o r e i t .
Logger . warn ( " \( TAG ) ignoring \( #function ) for call other than current call " )
return
}
guard let thread = self . thread else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) ignoring \( #function ) for call other than current call " ) )
return
}
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) missing peerconnection client in \( #function ) " ) )
return
}
let callRecord = TSCall ( timestamp : NSDate . ows_millisecondTimeStamp ( ) , withCallNumber : call . remotePhoneNumber , callType : RPRecentCallTypeIncoming , in : thread )
callRecord . save ( )
let message = DataChannelMessage . forConnected ( callId : call . signalingId )
peerConnectionClient . sendDataChannelMessage ( data : message . asData ( ) )
handleConnectedCall ( call )
}
/* *
* For outgoing call , when the callee has chosen to accept the call .
* For incoming call , when the local user has chosen to accept the call .
*/
func handleConnectedCall ( _ call : SignalCall ) {
Logger . debug ( " \( TAG ) in \( #function ) " )
AssertIsOnMainThread ( )
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) peerConnectionClient unexpectedly nil in \( #function ) " ) )
return
}
call . state = . connected
// W e d o n ' t r i s k t r a n s m i t t i n g a n y m e d i a u n t i l t h e r e m o t e c l i e n t h a s a d m i t t e d t o b e i n g c o n n e c t e d .
peerConnectionClient . setAudioEnabled ( enabled : ! call . isMuted )
peerConnectionClient . setLocalVideoEnabled ( enabled : shouldHaveLocalVideoTrack ( ) )
}
/* *
* Local user chose to decline the call vs . answering it .
*
* The call is referred to by call ` localId ` , which is included in Notification actions .
*
* Incoming call only .
*/
public func handleDeclineCall ( localId : UUID ) {
AssertIsOnMainThread ( )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) call was unexpectedly nil in \( #function ) " ) )
return
}
guard call . localId = = localId else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) callLocalId: \( localId ) doesn't match current calls: \( call . localId ) " ) )
return
}
self . handleDeclineCall ( call )
}
/* *
* Local user chose to decline the call vs . answering it .
*
* Incoming call only .
*/
public func handleDeclineCall ( _ call : SignalCall ) {
AssertIsOnMainThread ( )
Logger . info ( " \( TAG ) in \( #function ) " )
// C u r r e n t l y w e j u s t h a n d l e t h i s a s a h a n g u p . B u t w e c o u l d o f f e r m o r e d e s c r i p t i v e a c t i o n . e . g . D a t a C h a n n e l m e s s a g e
handleLocalHungupCall ( call )
}
/* *
* Local user chose to end the call .
*
* Can be used for Incoming and Outgoing calls .
*/
func handleLocalHungupCall ( _ call : SignalCall ) {
AssertIsOnMainThread ( )
guard self . call != nil else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) ignoring \( #function ) since there is no current call " ) )
return
}
guard call = = self . call ! else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) ignoring \( #function ) for call other than current call " ) )
return
}
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) missing peerconnection client in \( #function ) " ) )
return
}
guard let thread = self . thread else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) missing thread in \( #function ) " ) )
return
}
call . state = . localHangup
// T O D O s o m e t h i n g l i k e t h i s l i f t e d f r o m S i g n a l - A n d r o i d .
// t h i s . a c c o u n t M a n a g e r . c a n c e l I n F l i g h t R e q u e s t s ( ) ;
// t h i s . m e s s a g e S e n d e r . c a n c e l I n F l i g h t R e q u e s t s ( ) ;
// I f t h e c a l l i s c o n n e c t e d , w e c a n s e n d t h e h a n g u p v i a t h e d a t a c h a n n e l .
let message = DataChannelMessage . forHangup ( callId : call . signalingId )
peerConnectionClient . sendDataChannelMessage ( data : message . asData ( ) )
// I f t h e c a l l h a s n ' t s t a r t e d y e t , w e d o n ' t h a v e a d a t a c h a n n e l t o c o m m u n i c a t e t h e h a n g u p . U s e S i g n a l S e r v i c e M e s s a g e .
let hangupMessage = OWSCallHangupMessage ( callId : call . signalingId )
let callMessage = OWSOutgoingCallMessage ( thread : thread , hangupMessage : hangupMessage )
_ = self . messageSender . sendCallMessage ( callMessage ) . then {
Logger . debug ( " \( self . TAG ) successfully sent hangup call message to \( thread ) " )
} . catch { error in
Logger . error ( " \( self . TAG ) failed to send hangup call message to \( thread ) with error: \( error ) " )
}
terminateCall ( )
}
/* *
* Local user toggled to mute audio .
*
* Can be used for Incoming and Outgoing calls .
*/
func setIsMuted ( isMuted : Bool ) {
AssertIsOnMainThread ( )
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) peerConnectionClient unexpectedly nil in \( #function ) " ) )
return
}
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) call unexpectedly nil in \( #function ) " ) )
return
}
call . isMuted = isMuted
peerConnectionClient . setAudioEnabled ( enabled : ! isMuted )
}
/* *
* Local user toggled video .
*
* Can be used for Incoming and Outgoing calls .
*/
func setHasLocalVideo ( hasLocalVideo : Bool ) {
AssertIsOnMainThread ( )
let authStatus = AVCaptureDevice . authorizationStatus ( forMediaType : AVMediaTypeVideo )
switch authStatus {
case . notDetermined :
Logger . debug ( " \( TAG ) authStatus: AVAuthorizationStatusNotDetermined " )
break
case . restricted :
Logger . debug ( " \( TAG ) authStatus: AVAuthorizationStatusRestricted " )
break
case . denied :
Logger . debug ( " \( TAG ) authStatus: AVAuthorizationStatusDenied " )
break
case . authorized :
Logger . debug ( " \( TAG ) authStatus: AVAuthorizationStatusAuthorized " )
break
}
// W e d o n ' t n e e d t o w o r r y a b o u t t h e u s e r g r a n t i n g o r r e m o t i n g t h i s p e r m i s s i o n
// d u r i n g a c a l l w h i l e t h e a p p i s i n t h e b a c k g r o u n d , b e c a u s e c h a n g i n g t h i s
// p e r m i s s i o n k i l l s t h e a p p .
if authStatus != . authorized {
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 ( )
return
}
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) peerConnectionClient unexpectedly nil in \( #function ) " ) )
return
}
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) call unexpectedly nil in \( #function ) " ) )
return
}
call . hasLocalVideo = hasLocalVideo
peerConnectionClient . setLocalVideoEnabled ( enabled : shouldHaveLocalVideoTrack ( ) )
}
func handleCallKitStartVideo ( ) {
AssertIsOnMainThread ( )
self . setHasLocalVideo ( hasLocalVideo : true )
}
/* *
* 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
* action is idemptotent , there ' s no harm done .
*
* Used by both Incoming and Outgoing calls .
*/
private func handleDataChannelMessage ( _ message : OWSWebRTCProtosData ) {
AssertIsOnMainThread ( )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) received data message, but there is no current call. Ignoring. " ) )
return
}
if message . hasConnected ( ) {
Logger . debug ( " \( TAG ) remote participant sent Connected via data channel " )
let connected = message . connected !
guard connected . id = = call . signalingId else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) received connected message for call with id: \( connected . id ) but current call has id: \( call . signalingId ) " ) )
return
}
self . callUIAdapter . recipientAcceptedCall ( call )
handleConnectedCall ( call )
} else if message . hasHangup ( ) {
Logger . debug ( " \( TAG ) remote participant sent Hangup via data channel " )
let hangup = message . hangup !
guard hangup . id = = call . signalingId else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) received hangup message for call with id: \( hangup . id ) but current call has id: \( call . signalingId ) " ) )
return
}
guard let thread = self . thread else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) current contact thread is unexpectedly nil when receiving hangup DataChannelMessage " ) )
return
}
handleRemoteHangup ( thread : thread )
} else if message . hasVideoStreamingStatus ( ) {
Logger . debug ( " \( TAG ) remote participant sent VideoStreamingStatus via data channel " )
self . isRemoteVideoEnabled = message . videoStreamingStatus . enabled ( )
} else {
Logger . info ( " \( TAG ) received unknown or empty DataChannelMessage " )
}
}
// MARK: - P e e r C o n n e c t i o n C l i e n t D e l e g a t e
/* *
* The connection has been established . The clients can now communicate .
*/
internal func peerConnectionClientIceConnected ( _ peerConnectionClient : PeerConnectionClient ) {
AssertIsOnMainThread ( )
guard peerConnectionClient = = self . peerConnectionClient else {
Logger . debug ( " \( self . TAG ) \( #function ) Ignoring event from obsolete peerConnectionClient " )
return
}
self . handleIceConnected ( )
}
/* *
* The connection failed to establish . The clients will not be able to communicate .
*/
internal func peerConnectionClientIceFailed ( _ peerConnectionClient : PeerConnectionClient ) {
AssertIsOnMainThread ( )
guard peerConnectionClient = = self . peerConnectionClient else {
Logger . debug ( " \( self . TAG ) \( #function ) Ignoring event from obsolete peerConnectionClient " )
return
}
self . handleFailedCall ( error : CallError . disconnected )
}
/* *
* 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 .
*/
internal func peerConnectionClient ( _ peerConnectionClient : PeerConnectionClient , addedLocalIceCandidate iceCandidate : RTCIceCandidate ) {
AssertIsOnMainThread ( )
guard peerConnectionClient = = self . peerConnectionClient else {
Logger . debug ( " \( self . TAG ) \( #function ) Ignoring event from obsolete peerConnectionClient " )
return
}
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 ) {
AssertIsOnMainThread ( )
guard peerConnectionClient = = self . peerConnectionClient else {
Logger . debug ( " \( self . TAG ) \( #function ) Ignoring event from obsolete peerConnectionClient " )
return
}
self . handleDataChannelMessage ( dataChannelMessage )
}
internal func peerConnectionClient ( _ peerConnectionClient : PeerConnectionClient , didUpdateLocal videoTrack : RTCVideoTrack ? ) {
AssertIsOnMainThread ( )
guard peerConnectionClient = = self . peerConnectionClient else {
Logger . debug ( " \( self . TAG ) \( #function ) Ignoring event from obsolete peerConnectionClient " )
return
}
self . localVideoTrack = videoTrack
self . fireDidUpdateVideoTracks ( )
}
internal func peerConnectionClient ( _ peerConnectionClient : PeerConnectionClient , didUpdateRemote videoTrack : RTCVideoTrack ? ) {
AssertIsOnMainThread ( )
guard peerConnectionClient = = self . peerConnectionClient else {
Logger . debug ( " \( self . TAG ) \( #function ) Ignoring event from obsolete peerConnectionClient " )
return
}
self . remoteVideoTrack = videoTrack
self . fireDidUpdateVideoTracks ( )
}
// MARK: H e l p e r s
/* *
* 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 { turnServerInfo -> [ RTCIceServer ] in
Logger . debug ( " \( self . TAG ) got turn server urls: \( turnServerInfo . urls ) " )
return turnServerInfo . urls . map { url in
if url . hasPrefix ( " turn " ) {
// O n l y " t u r n : " s e r v e r s r e q u i r e a u t h e n t i c a t i o n . D o n ' t i n c l u d e t h e c r e d e n t i a l s t o o t h e r I C E s e r v e r s
// a s 1 . ) t h e y a r e n ' t u s e d , a n d 2 . ) t h e n o n - t u r n s e r v e r s m i g h t n o t b e u n d e r o u r c o n t r o l .
// e . g . w e u s e a p u b l i c f a l l b a c k S T U N s e r v e r .
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 ]
}
}
public func handleFailedCall ( error : CallError ) {
AssertIsOnMainThread ( )
Logger . error ( " \( TAG ) call failed with error: \( error ) " )
if let call = self . call {
// I t ' s e s s e n t i a l t o s e t c a l l . s t a t e b e f o r e t e r m i n a t e C a l l , b e c a u s e t e r m i n a t e C a l l n i l s s e l f . c a l l
call . error = error
call . state = . localFailure
self . callUIAdapter . failCall ( call , error : error )
} else {
// T h i s c a n h a p p e n w h e n w e r e c e i v e a n o u t o f b a n d s i g n a l i n g m e s s a g e ( e . g . I c e U p d a t e )
// a f t e r t h e c a l l h a s e n d e d
Logger . debug ( " \( TAG ) in \( #function ) but there was no call to fail. " )
}
terminateCall ( )
}
/* *
* Clean up any existing call state and get ready to receive a new call .
*/
private func terminateCall ( ) {
AssertIsOnMainThread ( )
Logger . debug ( " \( TAG ) in \( #function ) " )
localVideoTrack = nil
remoteVideoTrack = nil
isRemoteVideoEnabled = false
PeerConnectionClient . stopAudioSession ( )
peerConnectionClient ? . terminate ( )
Logger . debug ( " \( TAG ) setting peerConnectionClient in \( #function ) " )
peerConnectionClient = nil
call ? . removeAllObservers ( )
call = nil
thread = nil
incomingCallPromise = nil
sendIceUpdatesImmediately = true
pendingIceUpdateMessages = [ ]
}
// MARK: - C a l l O b s e r v e r
internal func stateDidChange ( call : SignalCall , state : CallState ) {
AssertIsOnMainThread ( )
Logger . info ( " \( self . TAG ) \( #function ) : \( state ) " )
self . updateIsVideoEnabled ( )
}
internal func hasLocalVideoDidChange ( call : SignalCall , hasLocalVideo : Bool ) {
AssertIsOnMainThread ( )
Logger . info ( " \( self . TAG ) \( #function ) : \( hasLocalVideo ) " )
self . updateIsVideoEnabled ( )
}
internal func muteDidChange ( call : SignalCall , isMuted : Bool ) {
AssertIsOnMainThread ( )
// D o n o t h i n g
}
internal func speakerphoneDidChange ( call : SignalCall , isEnabled : Bool ) {
AssertIsOnMainThread ( )
// D o n o t h i n g
}
// MARK: - V i d e o
private func shouldHaveLocalVideoTrack ( ) -> Bool {
AssertIsOnMainThread ( )
// T h e i O S s i m u l a t o r d o e s n ' t p r o v i d e a n y s o r t o f c a m e r a c a p t u r e
// s u p p o r t o r e m u l a t i o n ( h t t p : / / g o o . g l / r H A n C 1 ) s o d o n ' t b o t h e r
// t r y i n g t o o p e n a l o c a l s t r e a m .
return ( ! Platform . isSimulator &&
UIApplication . shared . applicationState != . background &&
call != nil &&
call ! . state = = . connected &&
call ! . hasLocalVideo )
}
private func updateIsVideoEnabled ( ) {
AssertIsOnMainThread ( )
guard let call = self . call else {
return
}
guard let peerConnectionClient = self . peerConnectionClient else {
return
}
let shouldHaveLocalVideoTrack = self . shouldHaveLocalVideoTrack ( )
Logger . info ( " \( self . TAG ) \( #function ) : \( shouldHaveLocalVideoTrack ) " )
self . peerConnectionClient ? . setLocalVideoEnabled ( enabled : shouldHaveLocalVideoTrack )
let message = DataChannelMessage . forVideoStreamingStatus ( callId : call . signalingId , enabled : shouldHaveLocalVideoTrack )
peerConnectionClient . sendDataChannelMessage ( data : message . asData ( ) )
}
// MARK: - O b s e r v e r s
// T h e o b s e r v e r - r e l a t e d m e t h o d s s h o u l d b e i n v o k e d o n t h e m a i n t h r e a d .
func addObserverAndSyncState ( observer : CallServiceObserver ) {
AssertIsOnMainThread ( )
observers . append ( Weak ( value : observer ) )
// S y n c h r o n i z e o b s e r v e r w i t h c u r r e n t c a l l s t a t e
let localVideoTrack = self . localVideoTrack
let remoteVideoTrack = self . isRemoteVideoEnabled ? self . remoteVideoTrack : nil
observer . didUpdateVideoTracks ( localVideoTrack : localVideoTrack ,
remoteVideoTrack : remoteVideoTrack )
}
// T h e o b s e r v e r - r e l a t e d m e t h o d s s h o u l d b e i n v o k e d o n t h e m a i n t h r e a d .
func removeObserver ( _ observer : CallServiceObserver ) {
AssertIsOnMainThread ( )
while let index = observers . index ( where : { $0 . value = = = observer } ) {
observers . remove ( at : index )
}
}
// T h e o b s e r v e r - r e l a t e d m e t h o d s s h o u l d b e i n v o k e d o n t h e m a i n t h r e a d .
func removeAllObservers ( ) {
AssertIsOnMainThread ( )
observers = [ ]
}
func fireDidUpdateVideoTracks ( ) {
AssertIsOnMainThread ( )
let localVideoTrack = self . localVideoTrack
let remoteVideoTrack = self . isRemoteVideoEnabled ? self . remoteVideoTrack : nil
for observer in observers {
observer . value ? . didUpdateVideoTracks ( localVideoTrack : localVideoTrack ,
remoteVideoTrack : remoteVideoTrack )
}
// P r e v e n t s c r e e n f r o m d i m m i n g d u r i n g v i d e o c a l l .
//
// f i r e D i d U p d a t e V i d e o T r a c k s ( ) i s c a l l e d b y t h e v i d e o t r a c k s e t t e r s ,
// w h i c h a r e c l e a r e d w h e n t h e c a l l e n d s . T h a t e n s u r e s t h a t t h i s t i m e r
// w i l l b e r e - e n a b l e d .
let hasLocalOrRemoteVideo = localVideoTrack != nil || remoteVideoTrack != nil
UIApplication . shared . isIdleTimerDisabled = hasLocalOrRemoteVideo
}
}
fileprivate extension MessageSender {
/* *
* Wrap message sending in a Promise for easier callback chaining .
*/
fileprivate func sendCallMessage ( _ message : OWSOutgoingCallMessage ) -> Promise < Void > {
return Promise { fulfill , reject in
self . send ( message , success : fulfill , failure : reject )
}
}
}