Added CallService documentation

// FREEBIE
pull/1/head
Michael Kirk 8 years ago committed by Matthew Chen
parent 602a5953f2
commit f9b44c8892

@ -307,25 +307,26 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
// TODO Something like... // TODO Something like...
// *phoneNumber = [[[[[[userActivity interaction] intent] contacts] firstObject] personHandle] value] // *phoneNumber = [[[[[[userActivity interaction] intent] contacts] firstObject] personHandle] value]
// thread = blah // 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; 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
//}
/** /**

@ -6,39 +6,62 @@ import PromiseKit
import WebRTC import WebRTC
/** /**
* ## Call Setup (Signaling) Flow * `CallService` manages the state of a WebRTC backed Signal Call (as opposed to the legacy "RedPhone Call").
* *
* ## Key * It serves as connection from the `CallUIAdapater` to the `PeerConnectionClient`.
* - SS: Message sent via Signal Service *
* - DC: Message sent via WebRTC Data Channel *
* ## 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 | * | Caller | Callee |
* +----------------------------+-------------------------+ * +----------------------------+-------------------------+
* handleOutgoingCall --[SS.CallOffer]--> * Start outgoing call: `handleOutgoingCall`
* and start storing ICE updates --[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 * Send call answer
* <--[SS.CallAnswer]-- * <--[SS.CallAnswer]--
* Start sending ICE updates immediately * Start generating ICE updates and send them as
* <--[SS.ICEUpdates]-- * they are generated: `handleLocalAddedIceCandidate`
* <--[SS.ICEUpdate]-- (sent multiple times)
* *
* Received CallAnswer, * Received CallAnswer: `handleReceivedAnswer`
* so send any stored ice updates * so send any stored ice updates
* --[SS.ICEUpdates]--> * --[SS.ICEUpdates]-->
* *
* Once compatible ICE updates have been exchanged... * Once compatible ICE updates have been exchanged...
* (ICE Connected) * both parties: `handleIceConnected`
* *
* Show remote ringing UI * Show remote ringing UI
* Connect to offered Data Channel * Connect to offered Data Channel
* Show incoming call UI. * Show incoming call UI.
* *
* Answers Call * If callee answers Call
* send connected message * send connected message
* <--[DC.ConnectedMesage]-- * <--[DC.ConnectedMesage]--
* Received connected message * Received connected message
* Show Call is connected. * Show Call is connected.
*
* Hang up (this could equally be sent by the Callee)
* --[DC.Hangup]-->
* --[SS.Hangup]-->
*/ */
enum CallError: Error { enum CallError: Error {
@ -74,11 +97,22 @@ fileprivate let timeoutSeconds = 60
// MARK: Ivars // MARK: Ivars
var peerConnectionClient: PeerConnectionClient? 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 thread: TSContactThread?
var call: SignalCall? 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 sendIceUpdatesImmediately = true
var pendingIceUpdateMessages = [OWSCallIceUpdateMessage]() var pendingIceUpdateMessages = [OWSCallIceUpdateMessage]()
// ensure the incoming call promise isn't dealloc'd prematurely
var incomingCallPromise: Promise<Void>? var incomingCallPromise: Promise<Void>?
// Used to coordinate promises across delegate methods // 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)") * User didn't answer incoming call
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) { public func handleMissedCall(_ call: SignalCall, thread: TSContactThread) {
// Insert missed call record // Insert missed call record
let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(),
@ -226,6 +252,23 @@ fileprivate let timeoutSeconds = 60
self.callUIAdapter.reportMissedCall(call) 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) { public func handleRemoteBusy(thread: TSContactThread) {
Logger.debug("\(TAG) \(#function) for thread: \(thread)") Logger.debug("\(TAG) \(#function) for thread: \(thread)")
assertOnSignalingQueue() assertOnSignalingQueue()
@ -239,11 +282,6 @@ fileprivate let timeoutSeconds = 60
terminateCall() 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 * Received an incoming call offer. We still have to complete setting up the Signaling channel before we notify
* the user of an incoming call. * 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) { public func handleCallBack(recipientId: String) {
// TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue) // TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue)
//assertOnSignalingQueue() //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) { public func handleRemoteAddedIceCandidate(thread: TSContactThread, callId: UInt64, sdp: String, lineIndex: Int32, mid: String) {
assertOnSignalingQueue() assertOnSignalingQueue()
Logger.debug("\(TAG) called \(#function)") Logger.debug("\(TAG) called \(#function)")
@ -368,6 +412,10 @@ fileprivate let timeoutSeconds = 60
peerConnectionClient.addIceCandidate(RTCIceCandidate(sdp: sdp, sdpMLineIndex: lineIndex, sdpMid: mid)) 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) { private func handleLocalAddedIceCandidate(_ iceCandidate: RTCIceCandidate) {
assertOnSignalingQueue() 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() { private func handleIceConnected() {
assertOnSignalingQueue() assertOnSignalingQueue()
@ -427,6 +481,7 @@ fileprivate let timeoutSeconds = 60
case .answering: case .answering:
call.state = .localRinging call.state = .localRinging
self.callUIAdapter.reportIncomingCall(call, thread: thread, audioManager: peerConnectionClient) self.callUIAdapter.reportIncomingCall(call, thread: thread, audioManager: peerConnectionClient)
// cancel connection timeout
self.fulfillCallConnectedPromise?() self.fulfillCallConnectedPromise?()
case .remoteRinging: case .remoteRinging:
Logger.info("\(TAG) call alreading ringing. Ignoring \(#function)") 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) { public func handleRemoteHangup(thread: TSContactThread) {
Logger.debug("\(TAG) in \(#function)") Logger.debug("\(TAG) in \(#function)")
assertOnSignalingQueue() 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) { public func handleAnswerCall(localId: UUID) {
// TODO #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue) // 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) { public func handleAnswerCall(_ call: SignalCall) {
assertOnSignalingQueue() assertOnSignalingQueue()
@ -533,8 +596,8 @@ fileprivate let timeoutSeconds = 60
} }
/** /**
* Called by initiator when recipient answers the call. * For outgoing call, when the callee has chosen to accept the call.
* Called by recipient upon answering the call. * For incoming call, when the local user has chosen to accept the call.
*/ */
func handleConnectedCall(_ call: SignalCall) { func handleConnectedCall(_ call: SignalCall) {
Logger.debug("\(TAG) in \(#function)") Logger.debug("\(TAG) in \(#function)")
@ -552,6 +615,13 @@ fileprivate let timeoutSeconds = 60
peerConnectionClient.setVideoEnabled(enabled: call.hasVideo) 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) { public func handleDeclineCall(localId: UUID) {
// #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue) // #function is called from objc, how to access swift defiend dispatch queue (OS_dispatch_queue)
//assertOnSignalingQueue() //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) { public func handleDeclineCall(_ call: SignalCall) {
assertOnSignalingQueue() assertOnSignalingQueue()
@ -582,6 +657,11 @@ fileprivate let timeoutSeconds = 60
handleLocalHungupCall(call) handleLocalHungupCall(call)
} }
/**
* Local user chose to end the call.
*
* Can be used for Incoming and Outgoing calls.
*/
func handleLocalHungupCall(_ call: SignalCall) { func handleLocalHungupCall(_ call: SignalCall) {
assertOnSignalingQueue() assertOnSignalingQueue()
@ -631,6 +711,11 @@ fileprivate let timeoutSeconds = 60
terminateCall() terminateCall()
} }
/**
* Local user toggled to mute audio.
*
* Can be used for Incoming and Outgoing calls.
*/
func handleToggledMute(isMuted: Bool) { func handleToggledMute(isMuted: Bool) {
assertOnSignalingQueue() assertOnSignalingQueue()
@ -641,6 +726,16 @@ fileprivate let timeoutSeconds = 60
peerConnectionClient.setAudioEnabled(enabled: !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) { private func handleDataChannelMessage(_ message: OWSWebRTCProtosData) {
assertOnSignalingQueue() assertOnSignalingQueue()
@ -691,6 +786,10 @@ fileprivate let timeoutSeconds = 60
// MARK: Helpers // 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() { private func assertOnSignalingQueue() {
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
dispatchPrecondition(condition: .onQueue(type(of: self).signalingQueue)) dispatchPrecondition(condition: .onQueue(type(of: self).signalingQueue))
@ -699,8 +798,10 @@ fileprivate let timeoutSeconds = 60
} }
} }
/**
*
*/
private func getIceServers() -> Promise<[RTCIceServer]> { private func getIceServers() -> Promise<[RTCIceServer]> {
return firstly { return firstly {
return accountManager.getTurnServerInfo() return accountManager.getTurnServerInfo()
}.then(on: CallService.signalingQueue) { turnServerInfo -> [RTCIceServer] in }.then(on: CallService.signalingQueue) { turnServerInfo -> [RTCIceServer] in
@ -708,7 +809,9 @@ fileprivate let timeoutSeconds = 60
return turnServerInfo.urls.map { url in return turnServerInfo.urls.map { url in
if url.hasPrefix("turn") { 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) return RTCIceServer(urlStrings: [url], username: turnServerInfo.username, credential: turnServerInfo.password)
} else { } else {
return RTCIceServer(urlStrings: [url]) return RTCIceServer(urlStrings: [url])

@ -8,6 +8,13 @@ import WebRTC
let kAudioTrackType = kRTCMediaStreamTrackKindAudio let kAudioTrackType = kRTCMediaStreamTrackKindAudio
let kVideoTrackType = kRTCMediaStreamTrackKindVideo 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 { class PeerConnectionClient: NSObject, SignalCallAudioManager {
let TAG = "[PeerConnectionClient]" let TAG = "[PeerConnectionClient]"
@ -28,7 +35,6 @@ class PeerConnectionClient: NSObject, SignalCallAudioManager {
// DataChannel // 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` // `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. // assigns it when the channel is discovered due to the caller having created it.
public var dataChannel: RTCDataChannel? public var dataChannel: RTCDataChannel?

Loading…
Cancel
Save