From f9b44c8892e856c38c9a35a23e3c4b856a5b82a3 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 6 Jan 2017 14:40:13 +0100 Subject: [PATCH] Added CallService documentation // FREEBIE --- Signal/src/AppDelegate.m | 33 ++-- Signal/src/call/CallService.swift | 171 +++++++++++++++++---- Signal/src/call/PeerConnectionClient.swift | 8 +- 3 files changed, 161 insertions(+), 51 deletions(-) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 60d007ddb..5ef70570c 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -307,25 +307,26 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; // TODO Something like... // *phoneNumber = [[[[[[userActivity interaction] intent] contacts] firstObject] personHandle] value] // thread = blah - // [callservice handleoutgoingCAll:thread] + // [callUIAdapter startCall:thread] // - // See Speakerbox Example for intent / NSUserActivity handling. + // Here's the Speakerbox Example for intent / NSUserActivity handling: + // + // func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + // guard let handle = userActivity.startCallHandle else { + // print("Could not determine start call handle from user activity: \(userActivity)") + // return false + // } + // + // guard let video = userActivity.video else { + // print("Could not determine video from user activity: \(userActivity)") + // return false + // } + // + // callManager.startCall(handle: handle, video: video) + // return true + // } return NO; } -//func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { -// guard let handle = userActivity.startCallHandle else { -// print("Could not determine start call handle from user activity: \(userActivity)") -// return false -// } -// -// guard let video = userActivity.video else { -// print("Could not determine video from user activity: \(userActivity)") -// return false -// } -// -// callManager.startCall(handle: handle, video: video) -// return true -//} /** diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index a5048f4dc..0801f1958 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -6,39 +6,62 @@ import PromiseKit import WebRTC /** - * ## Call Setup (Signaling) Flow + * `CallService` manages the state of a WebRTC backed Signal Call (as opposed to the legacy "RedPhone Call"). * - * ## Key - * - SS: Message sent via Signal Service - * - DC: Message sent via WebRTC Data Channel + * 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 | * +----------------------------+-------------------------+ - * handleOutgoingCall --[SS.CallOffer]--> - * and start storing ICE updates + * Start outgoing call: `handleOutgoingCall` + --[SS.CallOffer]--> + * and start generating and storing ICE updates. + * (As ICE candites are generated: `handleLocalAddedIceCandidate`) * - * Received call offer + * Received call offer: `handleReceivedOffer` * Send call answer * <--[SS.CallAnswer]-- - * Start sending ICE updates immediately - * <--[SS.ICEUpdates]-- + * Start generating ICE updates and send them as + * they are generated: `handleLocalAddedIceCandidate` + * <--[SS.ICEUpdate]-- (sent multiple times) * - * Received CallAnswer, + * Received CallAnswer: `handleReceivedAnswer` * so send any stored ice updates * --[SS.ICEUpdates]--> * * Once compatible ICE updates have been exchanged... - * (ICE Connected) + * both parties: `handleIceConnected` * * Show remote ringing UI * Connect to offered Data Channel * Show incoming call UI. * - * Answers Call + * 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 { @@ -74,11 +97,22 @@ fileprivate let timeoutSeconds = 60 // MARK: Ivars var peerConnectionClient: PeerConnectionClient? - // TODO move thread into SignalCall? Or refactor messageSender to take SignalRecipient + // TODO code cleanup: move thread into SignalCall? Or refactor messageSender to take SignalRecipient identifier. 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]() + + // ensure the incoming call promise isn't dealloc'd prematurely var incomingCallPromise: Promise? // Used to coordinate promises across delegate methods @@ -204,17 +238,9 @@ fileprivate let timeoutSeconds = 60 } } - 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) - } - + /** + * User didn't answer incoming call + */ public func handleMissedCall(_ call: SignalCall, thread: TSContactThread) { // Insert missed call record let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), @@ -226,6 +252,23 @@ fileprivate let timeoutSeconds = 60 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() @@ -239,11 +282,6 @@ fileprivate let timeoutSeconds = 60 terminateCall() } - private func isBusy() -> Bool { - // TODO CallManager adapter? - 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. @@ -319,6 +357,9 @@ fileprivate let timeoutSeconds = 60 } } + /** + * Initiate a call to recipient by recipientId + */ public func handleCallBack(recipientId: String) { // TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue) //assertOnSignalingQueue() @@ -336,6 +377,9 @@ fileprivate let timeoutSeconds = 60 } } + /** + * 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)") @@ -368,6 +412,10 @@ fileprivate let timeoutSeconds = 60 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() @@ -401,6 +449,12 @@ fileprivate let timeoutSeconds = 60 } } + /** + * 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() @@ -427,6 +481,7 @@ fileprivate let timeoutSeconds = 60 case .answering: call.state = .localRinging self.callUIAdapter.reportIncomingCall(call, thread: thread, audioManager: peerConnectionClient) + // cancel connection timeout self.fulfillCallConnectedPromise?() case .remoteRinging: Logger.info("\(TAG) call alreading ringing. Ignoring \(#function)") @@ -435,6 +490,9 @@ fileprivate let timeoutSeconds = 60 } } + /** + * The remote client (caller or callee) ended the call. + */ public func handleRemoteHangup(thread: TSContactThread) { Logger.debug("\(TAG) in \(#function)") assertOnSignalingQueue() @@ -467,7 +525,9 @@ fileprivate let timeoutSeconds = 60 } /** - * Answer call by call `localId`, used by notification actions which can't serialize a call object. + * 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) { // TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue) @@ -490,6 +550,9 @@ fileprivate let timeoutSeconds = 60 } } + /** + * User chose to answer call referrred to by call `localId`. Used by the Callee only. + */ public func handleAnswerCall(_ call: SignalCall) { assertOnSignalingQueue() @@ -533,8 +596,8 @@ fileprivate let timeoutSeconds = 60 } /** - * Called by initiator when recipient answers the call. - * Called by recipient upon answering the 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)") @@ -552,6 +615,13 @@ fileprivate let timeoutSeconds = 60 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) { // #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue) //assertOnSignalingQueue() @@ -573,6 +643,11 @@ fileprivate let timeoutSeconds = 60 } } + /** + * Local user chose to decline the call vs. answering it. + * + * Incoming call only. + */ public func handleDeclineCall(_ call: SignalCall) { assertOnSignalingQueue() @@ -582,6 +657,11 @@ fileprivate let timeoutSeconds = 60 handleLocalHungupCall(call) } + /** + * Local user chose to end the call. + * + * Can be used for Incoming and Outgoing calls. + */ func handleLocalHungupCall(_ call: SignalCall) { assertOnSignalingQueue() @@ -631,6 +711,11 @@ fileprivate let timeoutSeconds = 60 terminateCall() } + /** + * Local user toggled to mute audio. + * + * Can be used for Incoming and Outgoing calls. + */ func handleToggledMute(isMuted: Bool) { assertOnSignalingQueue() @@ -641,6 +726,16 @@ fileprivate let timeoutSeconds = 60 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() @@ -691,6 +786,10 @@ fileprivate let timeoutSeconds = 60 // MARK: Helpers + /** + * Ensure that all `SignalCall` and `CallService` state is synchronized by only mutating signaling state in + * handleXXX methods, and putting those methods on the signaling queue. + */ private func assertOnSignalingQueue() { if #available(iOS 10.0, *) { dispatchPrecondition(condition: .onQueue(type(of: self).signalingQueue)) @@ -699,8 +798,10 @@ fileprivate let timeoutSeconds = 60 } } + /** + * + */ private func getIceServers() -> Promise<[RTCIceServer]> { - return firstly { return accountManager.getTurnServerInfo() }.then(on: CallService.signalingQueue) { turnServerInfo -> [RTCIceServer] in @@ -708,7 +809,9 @@ fileprivate let timeoutSeconds = 60 return turnServerInfo.urls.map { url in if url.hasPrefix("turn") { - // only pass credentials for "turn:" servers. + // Only "turn:" servers require authentication. Don't include the credentials to other ICE servers + // as 1.) they aren't used, and 2.) the non-turn servers might not be under our control. + // e.g. we use a public fallback STUN server. return RTCIceServer(urlStrings: [url], username: turnServerInfo.username, credential: turnServerInfo.password) } else { return RTCIceServer(urlStrings: [url]) diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift index b90370771..15c9f6b0f 100644 --- a/Signal/src/call/PeerConnectionClient.swift +++ b/Signal/src/call/PeerConnectionClient.swift @@ -8,6 +8,13 @@ import WebRTC let kAudioTrackType = kRTCMediaStreamTrackKindAudio let kVideoTrackType = kRTCMediaStreamTrackKindVideo +/** + * `PeerConnectionClient` is our interface to WebRTC. + * + * It is primarily a wrapper around `RTCPeerConnection`, which is responsible for sending and receiving our call data + * including audio, video, and some signaling - though the bulk of the signaling is *establishing* the connection, + * meaning we can't use the connection to transmit yet. + */ class PeerConnectionClient: NSObject, SignalCallAudioManager { let TAG = "[PeerConnectionClient]" @@ -28,7 +35,6 @@ class PeerConnectionClient: NSObject, SignalCallAudioManager { // DataChannel - // peerConnection expects to be the final owner of dataChannel. Otherwise, a crash when peerConnection deallocs // `dataChannel` is public because on incoming calls, we don't explicitly create the channel, rather `CallService` // assigns it when the channel is discovered due to the caller having created it. public var dataChannel: RTCDataChannel?