// 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
/* *
* # # Call Setup ( Signaling ) Flow
*
* # # Key
* - SS : Message sent via Signal Service
* - DC : Message sent via WebRTC Data Channel
*
* | Caller | Callee |
* +----------------------------+-------------------------+
* handleOutgoingCall -- [ SS . CallOffer ] -->
* and start storing ICE updates
*
* Received call offer
* Send call answer
* <-- [ SS . CallAnswer ] --
* Start sending ICE updates immediately
* <-- [ SS . ICEUpdates ] --
*
* Received CallAnswer ,
* so send any stored ice updates
* -- [ SS . ICEUpdates ] -->
*
* Once compatible ICE updates have been exchanged . . .
* ( ICE Connected )
*
* Show remote ringing UI
* Connect to offered Data Channel
* Show incoming call UI .
*
* Answers Call
* send connected message
* <-- [ DC . ConnectedMesage ] --
* Received connected message
* Show Call is connected .
*/
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 , RTCDataChannelDelegate , RTCPeerConnectionDelegate {
// 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 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
var thread : TSContactThread ?
var call : SignalCall ?
var sendIceUpdatesImmediately = true
var pendingIceUpdateMessages = [ OWSCallIceUpdateMessage ] ( )
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 , peerConnectionDelegate : 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 . sendMessage ( 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 )
_ = sendMessage ( 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 )
}
}
}
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 )
_ = sendMessage ( callMessage )
handleMissedCall ( call , thread : thread )
}
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 )
}
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 ( )
}
private func isBusy ( ) -> Bool {
// T O D O C a l l M a n a g e r a d a p t e r ?
return false
}
/* *
* 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 , peerConnectionDelegate : 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
// T O D O ? W e b R t c C a l l S e r v i c e . t h i s . l o c k M a n a g e r . u p d a t e P h o n e S t a t e ( L o c k M a n a g e r . P h o n e S t a t e . P R O C E S S I N G ) ;
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 . sendMessage ( 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 )
}
}
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 )
}
}
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 ) )
}
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 )
_ = sendMessage ( 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
}
}
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
}
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) ignoring \( #function ) since there is no current peerConnectionClient. " ) )
return
}
switch call . state {
case . dialing :
call . state = . remoteRinging
case . answering :
call . state = . localRinging
self . callUIAdapter . reportIncomingCall ( call , thread : thread , audioManager : peerConnectionClient )
self . fulfillCallConnectedPromise ? ( )
case . remoteRinging :
Logger . info ( " \( TAG ) call alreading ringing. Ignoring \( #function ) " )
default :
Logger . debug ( " \( TAG ) unexpected call state for \( #function ) : \( call . state ) " )
}
}
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 ( )
}
/* *
* Answer call by call ` localId ` , 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 )
}
}
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 ( )
callUIAdapter . answerCall ( call )
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 )
}
/* *
* Called by initiator when recipient answers the call .
* Called by recipient upon answering 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 )
}
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 )
}
}
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 )
}
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 )
_ = sendMessage ( 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 ( )
}
func handleToggledMute ( isMuted : Bool ) {
assertOnSignalingQueue ( )
guard let peerConnectionClient = self . peerConnectionClient else {
handleFailedCall ( error : . assertionError ( description : " \( TAG ) peerConnectionClient unexpectedly nil in \( #function ) " ) )
return
}
peerConnectionClient . setAudioEnabled ( enabled : ! isMuted )
}
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: H e l p e r s
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 .
}
}
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 p a s s c r e d e n t i a l s f o r " t u r n : " s e r v e r s .
return RTCIceServer ( urlStrings : [ url ] , username : turnServerInfo . username , credential : turnServerInfo . password )
} else {
return RTCIceServer ( urlStrings : [ url ] )
}
} + [ CallService . fallbackIceServer ]
}
}
private func sendMessage ( _ message : OWSOutgoingCallMessage ) -> Promise < Void > {
return Promise { fulfill , reject in
self . messageSender . send ( message , success : fulfill , failure : reject )
}
}
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 ( )
}
private func terminateCall ( ) {
assertOnSignalingQueue ( )
// l o c k M a n a g e r . u p d a t e P h o n e S t a t e ( L o c k M a n a g e r . P h o n e S t a t e . P R O C E S S I N G ) ;
// N o t i f i c a t i o n B a r M a n a g e r . s e t C a l l E n d e d ( t h i s ) ;
//
// i n c o m i n g R i n g e r . s t o p ( ) ;
// o u t g o i n g R i n g e r . s t o p ( ) ;
// o u t g o i n g R i n g e r . p l a y D i s c o n n e c t e d ( ) ;
//
// i f ( p e e r C o n n e c t i o n ! = n u l l ) {
// p e e r C o n n e c t i o n . d i s p o s e ( ) ;
// p e e r C o n n e c t i o n = n u l l ;
// }
//
// i f ( e g l B a s e ! = n u l l & & l o c a l R e n d e r e r ! = n u l l & & r e m o t e R e n d e r e r ! = n u l l ) {
// l o c a l R e n d e r e r . r e l e a s e ( ) ;
// r e m o t e R e n d e r e r . r e l e a s e ( ) ;
// e g l B a s e . r e l e a s e ( ) ;
// }
//
// s h u t d o w n A u d i o ( ) ;
//
// t h i s . c a l l S t a t e = C a l l S t a t e . S T A T E _ I D L E ;
// t h i s . r e c i p i e n t = n u l l ;
// t h i s . c a l l I d = n u l l ;
// t h i s . a u d i o E n a b l e d = f a l s e ;
// t h i s . v i d e o E n a b l e d = f a l s e ;
// t h i s . p e n d i n g I c e U p d a t e s = n u l l ;
// l o c k M a n a g e r . u p d a t e P h o n e S t a t e ( L o c k M a n a g e r . P h o n e S t a t e . I D L E ) ;
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 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 ) " )
// S i g n a l i n g Q u e u e . d i s p a t c h . a s y n c { }
}
/* * 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 ) " )
}
// MARK: - R T C P e e r C o n n e c t i o n D e l e g a t e
/* * C a l l e d w h e n t h e S i g n a l i n g S t a t e c h a n g e d . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didChange stateChanged : RTCSignalingState ) {
Logger . debug ( " \( TAG ) didChange signalingState: \( stateChanged . debugDescription ) " )
}
/* * C a l l e d w h e n m e d i a i s r e c e i v e d o n a n e w s t r e a m f r o m r e m o t e p e e r . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didAdd stream : RTCMediaStream ) {
Logger . debug ( " \( TAG ) didAdd stream: \( stream ) " )
}
/* * C a l l e d w h e n a r e m o t e p e e r c l o s e s a s t r e a m . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didRemove stream : RTCMediaStream ) {
Logger . debug ( " \( TAG ) didRemove Stream: \( stream ) " )
}
/* * C a l l e d w h e n n e g o t i a t i o n i s n e e d e d , f o r e x a m p l e I C E h a s r e s t a r t e d . */
public func peerConnectionShouldNegotiate ( _ peerConnection : RTCPeerConnection ) {
Logger . debug ( " \( TAG ) shouldNegotiate " )
}
/* * C a l l e d a n y t i m e t h e I c e C o n n e c t i o n S t a t e c h a n g e s . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didChange newState : RTCIceConnectionState ) {
Logger . debug ( " \( TAG ) didChange IceConnectionState: \( newState . debugDescription ) " )
CallService . signalingQueue . async {
switch newState {
case . connected , . completed :
self . handleIceConnected ( )
case . failed :
Logger . warn ( " \( self . TAG ) RTCIceConnection failed. " )
guard self . thread != nil else {
Logger . error ( " \( self . TAG ) refusing to hangup for failed IceConnection because there is no current thread " )
return
}
self . handleFailedCall ( error : CallError . disconnected )
default :
Logger . debug ( " \( self . TAG ) ignoring change IceConnectionState: \( newState . debugDescription ) " )
}
}
}
/* * C a l l e d a n y t i m e t h e I c e G a t h e r i n g S t a t e c h a n g e s . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didChange newState : RTCIceGatheringState ) {
Logger . debug ( " \( TAG ) didChange IceGatheringState: \( newState . debugDescription ) " )
}
/* * N e w i c e c a n d i d a t e h a s b e e n f o u n d . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didGenerate candidate : RTCIceCandidate ) {
Logger . debug ( " \( TAG ) didGenerate IceCandidate: \( candidate . sdp ) " )
CallService . signalingQueue . async {
self . handleLocalAddedIceCandidate ( candidate )
}
}
/* * C a l l e d w h e n a g r o u p o f l o c a l I c e c a n d i d a t e s h a v e b e e n r e m o v e d . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didRemove candidates : [ RTCIceCandidate ] ) {
Logger . debug ( " \( TAG ) didRemove IceCandidates: \( candidates ) " )
}
/* * N e w d a t a c h a n n e l h a s b e e n o p e n e d . */
public func peerConnection ( _ peerConnection : RTCPeerConnection , didOpen dataChannel : RTCDataChannel ) {
Logger . debug ( " \( TAG ) didOpen dataChannel: \( dataChannel ) " )
CallService . signalingQueue . async {
guard let peerConnectionClient = self . peerConnectionClient else {
Logger . error ( " \( self . TAG ) surprised to find nil peerConnectionClient in \( #function ) " )
return
}
Logger . debug ( " \( self . TAG ) set dataChannel " )
peerConnectionClient . dataChannel = dataChannel
}
}
}
// M a r k : P r e t t y P r i n t O b j c e n u m s .
fileprivate extension RTCSignalingState {
var debugDescription : String {
switch self {
case . stable :
return " stable "
case . haveLocalOffer :
return " haveLocalOffer "
case . haveLocalPrAnswer :
return " haveLocalPrAnswer "
case . haveRemoteOffer :
return " haveRemoteOffer "
case . haveRemotePrAnswer :
return " haveRemotePrAnswer "
case . closed :
return " closed "
}
}
}
fileprivate extension RTCIceGatheringState {
var debugDescription : String {
switch self {
case . new :
return " new "
case . gathering :
return " gathering "
case . complete :
return " complete "
}
}
}
fileprivate extension RTCIceConnectionState {
var debugDescription : String {
switch self {
case . new :
return " new "
case . checking :
return " checking "
case . connected :
return " connected "
case . completed :
return " completed "
case . failed :
return " failed "
case . disconnected :
return " disconnected "
case . closed :
return " closed "
case . count :
return " count "
}
}
}