Skip to content

Commit 737ebd8

Browse files
committed
Added CallService documentation
// FREEBIE
1 parent 57d83d7 commit 737ebd8

File tree

3 files changed

+161
-51
lines changed

3 files changed

+161
-51
lines changed

Signal/src/AppDelegate.m

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -303,25 +303,26 @@ - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull N
303303
// TODO Something like...
304304
// *phoneNumber = [[[[[[userActivity interaction] intent] contacts] firstObject] personHandle] value]
305305
// thread = blah
306-
// [callservice handleoutgoingCAll:thread]
306+
// [callUIAdapter startCall:thread]
307307
//
308-
// See Speakerbox Example for intent / NSUserActivity handling.
308+
// Here's the Speakerbox Example for intent / NSUserActivity handling:
309+
//
310+
// func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
311+
// guard let handle = userActivity.startCallHandle else {
312+
// print("Could not determine start call handle from user activity: \(userActivity)")
313+
// return false
314+
// }
315+
//
316+
// guard let video = userActivity.video else {
317+
// print("Could not determine video from user activity: \(userActivity)")
318+
// return false
319+
// }
320+
//
321+
// callManager.startCall(handle: handle, video: video)
322+
// return true
323+
// }
309324
return NO;
310325
}
311-
//func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
312-
// guard let handle = userActivity.startCallHandle else {
313-
// print("Could not determine start call handle from user activity: \(userActivity)")
314-
// return false
315-
// }
316-
//
317-
// guard let video = userActivity.video else {
318-
// print("Could not determine video from user activity: \(userActivity)")
319-
// return false
320-
// }
321-
//
322-
// callManager.startCall(handle: handle, video: video)
323-
// return true
324-
//}
325326

326327

327328
/**

Signal/src/call/CallService.swift

Lines changed: 137 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,62 @@ import PromiseKit
66
import WebRTC
77

88
/**
9-
* ## Call Setup (Signaling) Flow
9+
* `CallService` manages the state of a WebRTC backed Signal Call (as opposed to the legacy "RedPhone Call").
1010
*
11-
* ## Key
12-
* - SS: Message sent via Signal Service
13-
* - DC: Message sent via WebRTC Data Channel
11+
* It serves as connection from the `CallUIAdapater` to the `PeerConnectionClient`.
12+
*
13+
*
14+
* ## Signaling
15+
*
16+
* Signaling refers to the setup and tear down of the connection. Before the connection is established, this must happen
17+
* out of band (using Signal Service), but once the connection is established it's possible to publish updates
18+
* (like hangup) via the established channel.
19+
*
20+
* Following is a high level process of the exchange of messages that must take place for this to happen.
21+
*
22+
* ### Key
23+
*
24+
* --[SOMETHING]--> represents a message of type "Something" sent from the caller to the callee
25+
* <--[SOMETHING]-- represents a message of type "Something" sent from the callee to the caller
26+
* SS: Message sent via Signal Service
27+
* DC: Message sent via WebRTC Data Channel
28+
*
29+
* ### Message Exchange / State Flow Overview
1430
*
1531
* | Caller | Callee |
1632
* +----------------------------+-------------------------+
17-
* handleOutgoingCall --[SS.CallOffer]-->
18-
* and start storing ICE updates
33+
* Start outgoing call: `handleOutgoingCall`
34+
--[SS.CallOffer]-->
35+
* and start generating and storing ICE updates.
36+
* (As ICE candites are generated: `handleLocalAddedIceCandidate`)
1937
*
20-
* Received call offer
38+
* Received call offer: `handleReceivedOffer`
2139
* Send call answer
2240
* <--[SS.CallAnswer]--
23-
* Start sending ICE updates immediately
24-
* <--[SS.ICEUpdates]--
41+
* Start generating ICE updates and send them as
42+
* they are generated: `handleLocalAddedIceCandidate`
43+
* <--[SS.ICEUpdate]-- (sent multiple times)
2544
*
26-
* Received CallAnswer,
45+
* Received CallAnswer: `handleReceivedAnswer`
2746
* so send any stored ice updates
2847
* --[SS.ICEUpdates]-->
2948
*
3049
* Once compatible ICE updates have been exchanged...
31-
* (ICE Connected)
50+
* both parties: `handleIceConnected`
3251
*
3352
* Show remote ringing UI
3453
* Connect to offered Data Channel
3554
* Show incoming call UI.
3655
*
37-
* Answers Call
56+
* If callee answers Call
3857
* send connected message
3958
* <--[DC.ConnectedMesage]--
4059
* Received connected message
4160
* Show Call is connected.
61+
*
62+
* Hang up (this could equally be sent by the Callee)
63+
* --[DC.Hangup]-->
64+
* --[SS.Hangup]-->
4265
*/
4366

4467
enum CallError: Error {
@@ -74,11 +97,22 @@ fileprivate let timeoutSeconds = 60
7497
// MARK: Ivars
7598

7699
var peerConnectionClient: PeerConnectionClient?
77-
// TODO move thread into SignalCall? Or refactor messageSender to take SignalRecipient
100+
// TODO code cleanup: move thread into SignalCall? Or refactor messageSender to take SignalRecipient identifier.
78101
var thread: TSContactThread?
79102
var call: SignalCall?
103+
104+
/**
105+
* In the process of establishing a connection between the clients (ICE process) we must exchange ICE updates.
106+
* Because this happens via Signal Service it's possible the callee user has not accepted any change in the caller's
107+
* identity. In which case *each* ICE update would cause an "identity change" warning on the callee's device. Since
108+
* this could be several messages, the caller stores all ICE updates until receiving positive confirmation that the
109+
* callee has received a message from us. This positive confirmation comes in the form of the callees `CallAnswer`
110+
* message.
111+
*/
80112
var sendIceUpdatesImmediately = true
81113
var pendingIceUpdateMessages = [OWSCallIceUpdateMessage]()
114+
115+
// ensure the incoming call promise isn't dealloc'd prematurely
82116
var incomingCallPromise: Promise<Void>?
83117

84118
// Used to coordinate promises across delegate methods
@@ -204,17 +238,9 @@ fileprivate let timeoutSeconds = 60
204238
}
205239
}
206240

207-
private func handleLocalBusyCall(_ call: SignalCall, thread: TSContactThread) {
208-
Logger.debug("\(TAG) \(#function) for call: \(call) thread: \(thread)")
209-
assertOnSignalingQueue()
210-
211-
let busyMessage = OWSCallBusyMessage(callId: call.signalingId)
212-
let callMessage = OWSOutgoingCallMessage(thread: thread, busyMessage: busyMessage)
213-
_ = sendMessage(callMessage)
214-
215-
handleMissedCall(call, thread: thread)
216-
}
217-
241+
/**
242+
* User didn't answer incoming call
243+
*/
218244
public func handleMissedCall(_ call: SignalCall, thread: TSContactThread) {
219245
// Insert missed call record
220246
let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(),
@@ -226,6 +252,23 @@ fileprivate let timeoutSeconds = 60
226252
self.callUIAdapter.reportMissedCall(call)
227253
}
228254

255+
/**
256+
* Received a call while already in another call.
257+
*/
258+
private func handleLocalBusyCall(_ call: SignalCall, thread: TSContactThread) {
259+
Logger.debug("\(TAG) \(#function) for call: \(call) thread: \(thread)")
260+
assertOnSignalingQueue()
261+
262+
let busyMessage = OWSCallBusyMessage(callId: call.signalingId)
263+
let callMessage = OWSOutgoingCallMessage(thread: thread, busyMessage: busyMessage)
264+
_ = messageSender.sendCallMessage(callMessage)
265+
266+
handleMissedCall(call, thread: thread)
267+
}
268+
269+
/**
270+
* The callee was already in another call.
271+
*/
229272
public func handleRemoteBusy(thread: TSContactThread) {
230273
Logger.debug("\(TAG) \(#function) for thread: \(thread)")
231274
assertOnSignalingQueue()
@@ -239,11 +282,6 @@ fileprivate let timeoutSeconds = 60
239282
terminateCall()
240283
}
241284

242-
private func isBusy() -> Bool {
243-
// TODO CallManager adapter?
244-
return false
245-
}
246-
247285
/**
248286
* Received an incoming call offer. We still have to complete setting up the Signaling channel before we notify
249287
* the user of an incoming call.
@@ -319,6 +357,9 @@ fileprivate let timeoutSeconds = 60
319357
}
320358
}
321359

360+
/**
361+
* Initiate a call to recipient by recipientId
362+
*/
322363
public func handleCallBack(recipientId: String) {
323364
// TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue)
324365
//assertOnSignalingQueue()
@@ -336,6 +377,9 @@ fileprivate let timeoutSeconds = 60
336377
}
337378
}
338379

380+
/**
381+
* Remote client (could be caller or callee) sent us a connectivity update
382+
*/
339383
public func handleRemoteAddedIceCandidate(thread: TSContactThread, callId: UInt64, sdp: String, lineIndex: Int32, mid: String) {
340384
assertOnSignalingQueue()
341385
Logger.debug("\(TAG) called \(#function)")
@@ -368,6 +412,10 @@ fileprivate let timeoutSeconds = 60
368412
peerConnectionClient.addIceCandidate(RTCIceCandidate(sdp: sdp, sdpMLineIndex: lineIndex, sdpMid: mid))
369413
}
370414

415+
/**
416+
* Local client (could be caller or callee) generated some connectivity information that we should send to the
417+
* remote client.
418+
*/
371419
private func handleLocalAddedIceCandidate(_ iceCandidate: RTCIceCandidate) {
372420
assertOnSignalingQueue()
373421

@@ -401,6 +449,12 @@ fileprivate let timeoutSeconds = 60
401449
}
402450
}
403451

452+
/**
453+
* The clients can now communicate via WebRTC.
454+
*
455+
* Called by both caller and callee. Compatible ICE messages have been exchanged between the local and remote
456+
* client.
457+
*/
404458
private func handleIceConnected() {
405459
assertOnSignalingQueue()
406460

@@ -427,6 +481,7 @@ fileprivate let timeoutSeconds = 60
427481
case .answering:
428482
call.state = .localRinging
429483
self.callUIAdapter.reportIncomingCall(call, thread: thread, audioManager: peerConnectionClient)
484+
// cancel connection timeout
430485
self.fulfillCallConnectedPromise?()
431486
case .remoteRinging:
432487
Logger.info("\(TAG) call alreading ringing. Ignoring \(#function)")
@@ -435,6 +490,9 @@ fileprivate let timeoutSeconds = 60
435490
}
436491
}
437492

493+
/**
494+
* The remote client (caller or callee) ended the call.
495+
*/
438496
public func handleRemoteHangup(thread: TSContactThread) {
439497
Logger.debug("\(TAG) in \(#function)")
440498
assertOnSignalingQueue()
@@ -467,7 +525,9 @@ fileprivate let timeoutSeconds = 60
467525
}
468526

469527
/**
470-
* Answer call by call `localId`, used by notification actions which can't serialize a call object.
528+
* User chose to answer call referrred to by call `localId`. Used by the Callee only.
529+
*
530+
* Used by notification actions which can't serialize a call object.
471531
*/
472532
public func handleAnswerCall(localId: UUID) {
473533
// TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue)
@@ -490,6 +550,9 @@ fileprivate let timeoutSeconds = 60
490550
}
491551
}
492552

553+
/**
554+
* User chose to answer call referrred to by call `localId`. Used by the Callee only.
555+
*/
493556
public func handleAnswerCall(_ call: SignalCall) {
494557
assertOnSignalingQueue()
495558

@@ -533,8 +596,8 @@ fileprivate let timeoutSeconds = 60
533596
}
534597

535598
/**
536-
* Called by initiator when recipient answers the call.
537-
* Called by recipient upon answering the call.
599+
* For outgoing call, when the callee has chosen to accept the call.
600+
* For incoming call, when the local user has chosen to accept the call.
538601
*/
539602
func handleConnectedCall(_ call: SignalCall) {
540603
Logger.debug("\(TAG) in \(#function)")
@@ -552,6 +615,13 @@ fileprivate let timeoutSeconds = 60
552615
peerConnectionClient.setVideoEnabled(enabled: call.hasVideo)
553616
}
554617

618+
/**
619+
* Local user chose to decline the call vs. answering it.
620+
*
621+
* The call is referred to by call `localId`, which is included in Notification actions.
622+
*
623+
* Incoming call only.
624+
*/
555625
public func handleDeclineCall(localId: UUID) {
556626
// #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue)
557627
//assertOnSignalingQueue()
@@ -573,6 +643,11 @@ fileprivate let timeoutSeconds = 60
573643
}
574644
}
575645

646+
/**
647+
* Local user chose to decline the call vs. answering it.
648+
*
649+
* Incoming call only.
650+
*/
576651
public func handleDeclineCall(_ call: SignalCall) {
577652
assertOnSignalingQueue()
578653

@@ -582,6 +657,11 @@ fileprivate let timeoutSeconds = 60
582657
handleLocalHungupCall(call)
583658
}
584659

660+
/**
661+
* Local user chose to end the call.
662+
*
663+
* Can be used for Incoming and Outgoing calls.
664+
*/
585665
func handleLocalHungupCall(_ call: SignalCall) {
586666
assertOnSignalingQueue()
587667

@@ -631,6 +711,11 @@ fileprivate let timeoutSeconds = 60
631711
terminateCall()
632712
}
633713

714+
/**
715+
* Local user toggled to mute audio.
716+
*
717+
* Can be used for Incoming and Outgoing calls.
718+
*/
634719
func handleToggledMute(isMuted: Bool) {
635720
assertOnSignalingQueue()
636721

@@ -641,6 +726,16 @@ fileprivate let timeoutSeconds = 60
641726
peerConnectionClient.setAudioEnabled(enabled: !isMuted)
642727
}
643728

729+
/**
730+
* Local client received a message on the WebRTC data channel.
731+
*
732+
* The WebRTC data channel is a faster signaling channel than out of band Signal Service messages. Once it's
733+
* established we use it to communicate further signaling information. The one sort-of exception is that with
734+
* hangup messages we redundantly send a Signal Service hangup message, which is more reliable, and since the hangup
735+
* action is idemptotent, there's no harm done.
736+
*
737+
* Used by both Incoming and Outgoing calls.
738+
*/
644739
private func handleDataChannelMessage(_ message: OWSWebRTCProtosData) {
645740
assertOnSignalingQueue()
646741

@@ -691,6 +786,10 @@ fileprivate let timeoutSeconds = 60
691786

692787
// MARK: Helpers
693788

789+
/**
790+
* Ensure that all `SignalCall` and `CallService` state is synchronized by only mutating signaling state in
791+
* handleXXX methods, and putting those methods on the signaling queue.
792+
*/
694793
private func assertOnSignalingQueue() {
695794
if #available(iOS 10.0, *) {
696795
dispatchPrecondition(condition: .onQueue(type(of: self).signalingQueue))
@@ -699,16 +798,20 @@ fileprivate let timeoutSeconds = 60
699798
}
700799
}
701800

801+
/**
802+
*
803+
*/
702804
private func getIceServers() -> Promise<[RTCIceServer]> {
703-
704805
return firstly {
705806
return accountManager.getTurnServerInfo()
706807
}.then(on: CallService.signalingQueue) { turnServerInfo -> [RTCIceServer] in
707808
Logger.debug("\(self.TAG) got turn server urls: \(turnServerInfo.urls)")
708809

709810
return turnServerInfo.urls.map { url in
710811
if url.hasPrefix("turn") {
711-
// only pass credentials for "turn:" servers.
812+
// Only "turn:" servers require authentication. Don't include the credentials to other ICE servers
813+
// as 1.) they aren't used, and 2.) the non-turn servers might not be under our control.
814+
// e.g. we use a public fallback STUN server.
712815
return RTCIceServer(urlStrings: [url], username: turnServerInfo.username, credential: turnServerInfo.password)
713816
} else {
714817
return RTCIceServer(urlStrings: [url])

0 commit comments

Comments
 (0)