// C r e a t e d b y M i c h a e l K i r k o n 1 1 / 1 1 / 1 6 .
// C o p y r i g h t © 2 0 1 6 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 ` manages the state of a WebRTC backed Signal Call ( as opposed to the legacy " RedPhone Call " ) .
*
* It serves as connection from the ` CallUIAdapater ` to 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 .
*
* Following is a high level process of the exchange of messages that must take place for this to happen .
*
* # # # 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 and storing ICE updates .
* ( As ICE candites are generated : ` handleLocalAddedIceCandidate ` )
*
* Received call offer : ` handleReceivedOffer `
* Send call answer
* <-- [ SS . CallAnswer ] --
* Start generating ICE updates and send them as
* they are generated : ` handleLocalAddedIceCandidate `
* <-- [ SS . ICEUpdate ] -- ( sent multiple times )
*
* Received CallAnswer : ` handleReceivedAnswer `
* so send any stored ice updates
* -- [ 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
@objc class CallService : NSObject , PeerConnectionClientDelegate , RTCDataChannelDelegate {
// MARK: - P r o p e r t i e s
let TAG = " [CallService] "
// MARK: D e p e n d e n c i e s
let accountManager : AccountManager
let messageSender : MessageSender
var callUIAdapter : CallUIAdapter !
// MARK: C l a s s
static let fallbackIceServer = RTCIceServer ( urlStrings : [ " stun:stun1.l.google.com:19302 " ] )
// S y n c h r o n i z e c a l l s i g n a l i n g o n t h e c a l l S i g n a l i n g Q u e u e t o m a k e s u r e a n y a p p r o p r i a t e r e q u i s i t e s t a t e i s s e t .
static let signalingQueue = DispatchQueue ( label : " CallServiceSignalingQueue " )
// MARK: I v a r s
var peerConnectionClient : PeerConnectionClient ?
// 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 ?
/* *
* 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 : ( ( ) -> ( ) ) ?
required init ( accountManager : AccountManager , contactsManager : OWSContactsManager , messageSender : MessageSender , notificationsAdapter : CallNotificationsAdapter ) {
self . accountManager = accountManager
self . messageSender = messageSender
super . init ( )
self . callUIAdapter = CallUIAdapter ( callService : self , contactsManager : contactsManager , notificationsAdapter : 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
// U n l e s s o t h e r w i s e d o c u m e n t e d , t h e s e ` h a n d l e X X X ` m e t h o d s e x p e c t t o b e c a l l e d o n t h e S i g n a l i n g Q u e u e t o c o o r d i n a t e
// s t a t e a c r o s s c a l l s .
/* *
* Initiate an outgoing call .
*/
public func handleOutgoingCall ( _ call : SignalCall ) -> Promise < Void > {
assertOnSignalingQueue ( )
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 ( on : CallService . signalingQueue ) { iceServers -> Promise < HardenedRTCSessionDescription > in
Logger . debug ( " \( self . TAG ) got ice servers: \( iceServers ) " )
let peerConnectionClient = PeerConnectionClient ( iceServers : iceServers , delegate : self )
self . peerConnectionClient = peerConnectionClient
// W h e n c a l l i n g , 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 e i v e r s 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 .
self . peerConnectionClient ! . createSignalingDataChannel ( delegate : self )
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 ) " )
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 ) " )
assertOnSignalingQueue ( )
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 ( 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 )
}
}
}
/* *
* User didn ' t answer incoming call
*/
public func handleMissedCall ( _ call : SignalCall , thread : TSContactThread ) {
// 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 ) " )
assertOnSignalingQueue ( )
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 ) " )
assertOnSignalingQueue ( )
guard let call = self . call else {
handleFailedCall ( error : . assertionError ( description : " call unexpectedly nil in \( #function ) " ) )
return
}
call . state = . remoteBusy
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 ) {
assertOnSignalingQueue ( )
Logger . verbose ( " \( TAG ) receivedCallOffer for thread: \( thread ) " )
let newCall = SignalCall . incomingCall ( localId : UUID ( ) , remotePhoneNumber : thread . contactIdentifier ( ) , signalingId : callId )
guard call = = nil 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. " )
CallService . signalingQueue . async {
self . handleFailedCall ( error : timeout )
}
}
incomingCallPromise = firstly {
return getIceServers ( )
} . then ( on : CallService . signalingQueue ) { ( 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 .
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 ( on : CallService . signalingQueue ) { ( 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 ( on : CallService . signalingQueue ) {
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 ( 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 )
}
}
/* *
* Initiate a call to recipient by recipientId
*/
public func handleCallBack ( recipientId : String ) {
// T O D O # f u n c t i o n i s c a l l e d f r o m o b j c , h o w t o a c c e s s s w i f t d e f i e n d d i s p a t c h q u e u e ( O S _ d i s p a t c h _ q u e u e )
// a s s e r t O n S i g n a l i n g Q u e u e ( )
guard self . call = = nil else {
Logger . error ( " \( TAG ) unexpectedly found an existing call when trying to call back: \( recipientId ) " )
return
}
// B e c a u s e w e m a y n o t b e o n s i g n a l i n g Q u e u e ( b e c a u s e t h i s m e t h o d i s c a l l e d f r o m O b j c w h i c h d o e s n ' t h a v e
// a c c e s s t o s i g n a l i n g Q u e u e ( t h a t I c a n f i n d ) . F I X M E ?
type ( of : self ) . signalingQueue . async {
let call = self . callUIAdapter . startOutgoingCall ( handle : recipientId )
self . callUIAdapter . showCall ( call )
}
}
/* *
* 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 ( )
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. TODO: Signaling messages out of order? " ) )
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 ) {
assertOnSignalingQueue ( )
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 ( ) {
assertOnSignalingQueue ( )
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 ) " )
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 ) " )
assertOnSignalingQueue ( )
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 . endCall ( 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 ) {
// T O D O # f u n c t i o n i s c a l l e d f r o m o b j c , h o w t o a c c e s s s w i f t d e f i e n d d i s p a t c h q u e u e ( O S _ d i s p a t c h _ q u e u e )
// a s s e r t O n S i g n a l i n g Q u e u e ( )
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
}
// B e c a u s e w e m a y n o t b e o n s i g n a l i n g Q u e u e ( b e c a u s e t h i s m e t h o d i s c a l l e d f r o m O b j c w h i c h d o e s n ' t h a v e
// a c c e s s t o s i g n a l i n g Q u e u e ( t h a t I c a n f i n d ) . F I X M E ?
type ( of : self ) . signalingQueue . async {
self . handleAnswerCall ( call )
}
}
/* *
* User chose to answer call referrred to by call ` localId ` . Used by the Callee only .
*/
public func handleAnswerCall ( _ call : SignalCall ) {
assertOnSignalingQueue ( )
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 )
if peerConnectionClient . sendDataChannelMessage ( data : message . asData ( ) ) {
Logger . debug ( " \( TAG ) sendDataChannelMessage returned true " )
} else {
Logger . warn ( " \( TAG ) sendDataChannelMessage returned false " )
}
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 ) " )
assertOnSignalingQueue ( )
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 : true )
peerConnectionClient . setVideoEnabled ( enabled : call . hasVideo )
}
/* *
* 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 ) {
// # f u n c t i o n i s c a l l e d f r o m o b j c , h o w t o a c c e s s s w i f t d e f i e n d d i s p a t c h q u e u e ( O S _ d i s p a t c h _ q u e u e )
// a s s e r t O n S i g n a l i n g Q u e u e ( )
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
}
// B e c a u s e w e m a y n o t b e o n s i g n a l i n g Q u e u e ( b e c a u s e t h i s m e t h o d i s c a l l e d f r o m O b j c w h i c h d o e s n ' t h a v e
// a c c e s s t o s i g n a l i n g Q u e u e ( t h a t I c a n f i n d ) . F I X M E ?
type ( of : self ) . signalingQueue . async {
self . handleDeclineCall ( call )
}
}
/* *
* Local user chose to decline the call vs . answering it .
*
* Incoming call only .
*/
public func handleDeclineCall ( _ call : SignalCall ) {
assertOnSignalingQueue ( )
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 ) {
assertOnSignalingQueue ( )
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 )
if peerConnectionClient . sendDataChannelMessage ( data : message . asData ( ) ) {
Logger . debug ( " \( TAG ) sendDataChannelMessage returned true " )
} else {
Logger . warn ( " \( TAG ) sendDataChannelMessage returned false " )
}
// 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 ( on : CallService . signalingQueue ) {
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 ) " )
}
terminateCall ( )
}
/* *
* Local user toggled to mute audio .
*
* Can be used for Incoming and Outgoing calls .
*/
func handleToggledMute ( isMuted : Bool ) {
assertOnSignalingQueue ( )
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 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 ) {
assertOnSignalingQueue ( )
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
}
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 " )
// TODO: t r a n s l a t e f r o m j a v a
// I n t e n t i n t e n t = n e w I n t e n t ( t h i s , W e b R t c C a l l S e r v i c e . c l a s s ) ;
// i n t e n t . s e t A c t i o n ( A C T I O N _ R E M O T E _ V I D E O _ M U T E ) ;
// i n t e n t . p u t E x t r a ( E X T R A _ C A L L _ I D , d a t a M e s s a g e . g e t V i d e o S t r e a m i n g S t a t u s ( ) . g e t I d ( ) ) ;
// i n t e n t . p u t E x t r a ( E X T R A _ M U T E , ! d a t a M e s s a g e . g e t V i d e o S t r e a m i n g S t a t u s ( ) . g e t E n a b l e d ( ) ) ;
// s t a r t S e r v i c e ( i n t e n t ) ;
}
}
// 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 ) {
CallService . signalingQueue . async {
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 )
}
}
/* *
* 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 ) {
CallService . signalingQueue . async {
self . handleLocalAddedIceCandidate ( iceCandidate )
}
}
// MARK: H e l p e r s
/* *
* 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 .
*/
private func assertOnSignalingQueue ( ) {
if #available ( iOS 10.0 , * ) {
dispatchPrecondition ( condition : . onQueue ( type ( of : self ) . signalingQueue ) )
} else {
// S k i p p i n g c h e c k o n < i O S 1 0 , s i n c e s y n t a x i s d i f f e r e n t a n d i t ' s j u s t a d e v e l o p m e n t c o n v e n i e n c e .
}
}
/* *
* 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 ] > {
return firstly {
return accountManager . getTurnServerInfo ( )
} . then ( on : CallService . signalingQueue ) { 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 ) {
assertOnSignalingQueue ( )
Logger . error ( " \( TAG ) call failed with error: \( error ) " )
// 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
terminateCall ( )
}
/* *
* Clean up any existing call state and get ready to receive a new call .
*/
private func terminateCall ( ) {
assertOnSignalingQueue ( )
Logger . debug ( " \( TAG ) in \( #function ) " )
peerConnectionClient ? . terminate ( )
peerConnectionClient = nil
call = nil
thread = nil
incomingCallPromise = nil
sendIceUpdatesImmediately = true
pendingIceUpdateMessages = [ ]
}
// MARK: - R T C D a t a C h a n n e l D e l e g a t e
// T O D O m o v e ` R T C D a t a C h a n n e l D e l e g a t e ` s t u f f i n t o p e e r C o n n e c t i o n C l i e n t a n d a d d a m e t h o d t o 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 ` r e c e i v e D a t a C h a n n e l M s s a g e ( _ m e s s a g e : O W S W e b R T C P r o t o s )
/* * T h e d a t a c h a n n e l s t a t e c h a n g e d . */
public func dataChannelDidChangeState ( _ dataChannel : RTCDataChannel ) {
Logger . debug ( " \( TAG ) dataChannelDidChangeState: \( dataChannel ) " )
}
/* * T h e d a t a c h a n n e l s u c c e s s f u l l y r e c e i v e d a d a t a b u f f e r . */
public func dataChannel ( _ dataChannel : RTCDataChannel , didReceiveMessageWith buffer : RTCDataBuffer ) {
Logger . debug ( " \( TAG ) dataChannel didReceiveMessageWith buffer: \( buffer ) " )
guard let dataChannelMessage = OWSWebRTCProtosData . parse ( from : buffer . data ) else {
// T O D O c a n ' t p r o t o p a r s i n g s t h r o w a n e x c e p t i o n ? I s i t j u s t b e i n g l o s t i n t h e O b j c - > S w i f t ?
Logger . error ( " \( TAG ) failed to parse dataProto " )
return
}
CallService . signalingQueue . async {
self . handleDataChannelMessage ( dataChannelMessage )
}
}
/* * T h e d a t a c h a n n e l ' s | b u f f e r e d A m o u n t | c h a n g e d . */
public func dataChannel ( _ dataChannel : RTCDataChannel , didChangeBufferedAmount amount : UInt64 ) {
Logger . debug ( " \( TAG ) didChangeBufferedAmount: \( amount ) " )
}
}
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 )
}
}
}