Merge pull request #1587 from WhisperSystems/mkirk/webrtc/call-kit-mute

Sync CallKit mute button state with in app mute button.
pull/1/head
Michael Kirk 8 years ago committed by GitHub
commit 2119f33f81

@ -347,6 +347,8 @@
<outlet property="contactAvatarView" destination="id3-xi-PFz" id="CUV-hJ-Qcp"/> <outlet property="contactAvatarView" destination="id3-xi-PFz" id="CUV-hJ-Qcp"/>
<outlet property="contactNameLabel" destination="UbL-8Z-oK1" id="h9V-l9-JVF"/> <outlet property="contactNameLabel" destination="UbL-8Z-oK1" id="h9V-l9-JVF"/>
<outlet property="incomingCallControls" destination="5E5-dq-23I" id="fWz-1n-pjI"/> <outlet property="incomingCallControls" destination="5E5-dq-23I" id="fWz-1n-pjI"/>
<outlet property="muteButton" destination="NES-Ce-PcK" id="Eku-Tx-cMN"/>
<outlet property="speakerPhoneButton" destination="Bb2-w8-mPU" id="GYN-Z6-i5I"/>
</connections> </connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="5mi-rT-gg5" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="5mi-rT-gg5" userLabel="First Responder" sceneMemberID="firstResponder"/>

@ -578,8 +578,6 @@ fileprivate let timeoutSeconds = 60
let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: call.remotePhoneNumber, callType: RPRecentCallTypeIncoming, in: thread) let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: call.remotePhoneNumber, callType: RPRecentCallTypeIncoming, in: thread)
callRecord.save() callRecord.save()
callUIAdapter.answerCall(call)
let message = DataChannelMessage.forConnected(callId: call.signalingId) let message = DataChannelMessage.forConnected(callId: call.signalingId)
if peerConnectionClient.sendDataChannelMessage(data: message.asData()) { if peerConnectionClient.sendDataChannelMessage(data: message.asData()) {
Logger.debug("\(TAG) sendDataChannelMessage returned true") Logger.debug("\(TAG) sendDataChannelMessage returned true")
@ -718,6 +716,13 @@ fileprivate let timeoutSeconds = 60
handleFailedCall(error: .assertionError(description:"\(TAG) peerConnectionClient unexpectedly nil in \(#function)")) handleFailedCall(error: .assertionError(description:"\(TAG) peerConnectionClient unexpectedly nil in \(#function)"))
return 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) peerConnectionClient.setAudioEnabled(enabled: !isMuted)
} }

@ -18,7 +18,7 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
self.notificationsAdapter = notificationsAdapter self.notificationsAdapter = notificationsAdapter
} }
public func startOutgoingCall(_ call: SignalCall) { func startOutgoingCall(_ call: SignalCall) {
CallService.signalingQueue.async { CallService.signalingQueue.async {
_ = self.callService.handleOutgoingCall(call).then { _ = self.callService.handleOutgoingCall(call).then {
Logger.debug("\(self.TAG) handleOutgoingCall succeeded") Logger.debug("\(self.TAG) handleOutgoingCall succeeded")
@ -28,7 +28,7 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
} }
} }
public func reportIncomingCall(_ call: SignalCall, callerName: String) { func reportIncomingCall(_ call: SignalCall, callerName: String) {
Logger.debug("\(TAG) \(#function)") Logger.debug("\(TAG) \(#function)")
// present Call View controller // present Call View controller
@ -43,23 +43,32 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee {
} }
} }
public func reportMissedCall(_ call: SignalCall, callerName: String) { func reportMissedCall(_ call: SignalCall, callerName: String) {
notificationsAdapter.presentMissedCall(call, callerName: callerName) notificationsAdapter.presentMissedCall(call, callerName: callerName)
} }
public func answerCall(_ call: SignalCall) { func answerCall(_ call: SignalCall) {
// NO-OP CallService.signalingQueue.async {
self.callService.handleAnswerCall(call)
}
} }
public func declineCall(_ call: SignalCall) { func declineCall(_ call: SignalCall) {
CallService.signalingQueue.async { CallService.signalingQueue.async {
self.callService.handleDeclineCall(call) self.callService.handleDeclineCall(call)
} }
} }
public func endCall(_ call: SignalCall) { func endCall(_ call: SignalCall) {
CallService.signalingQueue.async { CallService.signalingQueue.async {
self.callService.handleLocalHungupCall(call) self.callService.handleLocalHungupCall(call)
} }
} }
func toggleMute(call: SignalCall, isMuted: Bool) {
CallService.signalingQueue.async {
self.callService.handleToggledMute(isMuted: isMuted)
}
}
} }

@ -275,25 +275,6 @@ class PeerConnectionClient: NSObject {
let buffer = RTCDataBuffer(data: data, isBinary: false) let buffer = RTCDataBuffer(data: data, isBinary: false)
return dataChannel.sendData(buffer) return dataChannel.sendData(buffer)
} }
// MARK: CallAudioManager
internal func configureAudioSession() {
Logger.warn("TODO: \(#function)")
}
internal func stopAudio() {
Logger.warn("TODO: \(#function)")
}
internal func startAudio() {
guard let audioSender = self.audioSender else {
Logger.error("\(TAG) ignoring \(#function) because audioSender was nil")
return
}
Logger.warn("TODO: \(#function)")
}
} }
/** /**

@ -16,6 +16,11 @@ enum CallState: String {
case remoteBusy // terminal case remoteBusy // terminal
} }
protocol CallDelegate: class {
func stateDidChange(call: SignalCall, state: CallState)
func muteDidChange(call: SignalCall, isMuted: Bool)
}
/** /**
* Data model for a WebRTC backed voice/video call. * Data model for a WebRTC backed voice/video call.
*/ */
@ -23,21 +28,30 @@ enum CallState: String {
let TAG = "[SignalCall]" let TAG = "[SignalCall]"
weak var delegate: CallDelegate?
let remotePhoneNumber: String
// Signal Service identifier for this Call. Used to coordinate the call across remote clients.
let signalingId: UInt64
// Distinguishes between calls locally, e.g. in CallKit
let localId: UUID
var hasVideo = false
var state: CallState { var state: CallState {
didSet { didSet {
Logger.debug("\(TAG) state changed: \(oldValue) -> \(state)") Logger.debug("\(TAG) state changed: \(oldValue) -> \(state)")
stateDidChange?(state) delegate?.stateDidChange(call: self, state: state)
}
}
var isMuted = false {
didSet {
Logger.debug("\(TAG) muted changed: \(oldValue) -> \(isMuted)")
delegate?.muteDidChange(call: self, isMuted: isMuted)
} }
} }
let signalingId: UInt64
let remotePhoneNumber: String
let localId: UUID
var hasVideo = false
var error: CallError? var error: CallError?
var stateDidChange: ((_ newState: CallState) -> Void)?
init(localId: UUID, signalingId: UInt64, state: CallState, remotePhoneNumber: String) { init(localId: UUID, signalingId: UInt64, state: CallState, remotePhoneNumber: String) {
self.localId = localId self.localId = localId
self.signalingId = signalingId self.signalingId = signalingId

@ -5,12 +5,17 @@ import UIKit
import CallKit import CallKit
/** /**
* Based on SpeakerboxCallManager, from the Apple CallKit Example app. Though, it's responsibilities are mostly mirrored (and delegated from) CallUIAdapter? * Requests actions from CallKit
* TODO: Would it simplify things to merge this into CallKitCallUIAdaptee? *
* @Discussion:
* Based on SpeakerboxCallManager, from the Apple CallKit Example app. Though, it's responsibilities are mostly
* mirrored (and delegated from) CallKitCallUIAdaptee.
* TODO: Would it simplify things to merge this into CallKitCallUIAdaptee?
*/ */
@available(iOS 10.0, *) @available(iOS 10.0, *)
final class CallKitCallManager: NSObject { final class CallKitCallManager: NSObject {
let TAG = "[CallKitCallManager]"
let callController = CXCallController() let callController = CXCallController()
// MARK: Actions // MARK: Actions
@ -43,12 +48,28 @@ final class CallKitCallManager: NSObject {
requestTransaction(transaction) requestTransaction(transaction)
} }
func toggleMute(call: SignalCall, isMuted: Bool) {
let muteCallAction = CXSetMutedCallAction(call: call.localId, muted: isMuted)
let transaction = CXTransaction()
transaction.addAction(muteCallAction)
requestTransaction(transaction)
}
func answer(call: SignalCall) {
let answerCallAction = CXAnswerCallAction(call: call.localId)
let transaction = CXTransaction()
transaction.addAction(answerCallAction)
requestTransaction(transaction)
}
private func requestTransaction(_ transaction: CXTransaction) { private func requestTransaction(_ transaction: CXTransaction) {
callController.request(transaction) { error in callController.request(transaction) { error in
if let error = error { if let error = error {
print("Error requesting transaction: \(error)") Logger.error("\(self.TAG) Error requesting transaction: \(error)")
} else { } else {
print("Requested transaction successfully") Logger.debug("\(self.TAG) Requested transaction successfully")
} }
} }
} }

@ -7,10 +7,10 @@ import CallKit
import AVFoundation import AVFoundation
/** /**
* Connects user interface to the CallService usin CallKit. * Connects user interface to the CallService using CallKit.
* *
* User interface is mapped to CXCall action requests, and if the CXProvider accepts them, * User interface is routed to the CallManager which requests CXCallActions, and if the CXProvider accepts them,
* their corresponding consequences are requested via the CallService * their corresponding consequences are implmented in the CXProviderDelegate methods, e.g. using the CallService
*/ */
@available(iOS 10.0, *) @available(iOS 10.0, *)
final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
@ -55,14 +55,14 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
// MARK: CallUIAdaptee // MARK: CallUIAdaptee
internal func startOutgoingCall(_ call: SignalCall) { func startOutgoingCall(_ call: SignalCall) {
// Add the new outgoing call to the app's list of calls. // Add the new outgoing call to the app's list of calls.
// So we can find it in the provider delegate callbacks. // So we can find it in the provider delegate callbacks.
callManager.addCall(call) callManager.addCall(call)
callManager.startCall(call) callManager.startCall(call)
} }
internal func reportIncomingCall(_ call: SignalCall, callerName: String) { func reportIncomingCall(_ call: SignalCall, callerName: String) {
// Construct a CXCallUpdate describing the incoming call, including the caller. // Construct a CXCallUpdate describing the incoming call, including the caller.
let update = CXCallUpdate() let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber) update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber)
@ -83,18 +83,22 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
} }
} }
internal func answerCall(_ call: SignalCall) { func answerCall(_ call: SignalCall) {
showCall(call) callManager.answer(call: call)
} }
internal func declineCall(_ call: SignalCall) { func declineCall(_ call: SignalCall) {
callManager.end(call: call) callManager.end(call: call)
} }
internal func endCall(_ call: SignalCall) { func endCall(_ call: SignalCall) {
callManager.end(call: call) callManager.end(call: call)
} }
func toggleMute(call: SignalCall, isMuted: Bool) {
callManager.toggleMute(call: call, isMuted: isMuted)
}
// MARK: CXProviderDelegate // MARK: CXProviderDelegate
func providerDidReset(_ provider: CXProvider) { func providerDidReset(_ provider: CXProvider) {
@ -140,9 +144,9 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
CallService.signalingQueue.async { CallService.signalingQueue.async {
self.callService.handleOutgoingCall(call).then { self.callService.handleOutgoingCall(call).then {
action.fulfill() action.fulfill()
}.catch { error in }.catch { error in
self.callManager.removeCall(call) self.callManager.removeCall(call)
action.fail() action.fail()
} }
} }
@ -178,16 +182,14 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
// // Trigger the call to be answered via the underlying network service. // // Trigger the call to be answered via the underlying network service.
// call.answerSpeakerboxCall() // call.answerSpeakerboxCall()
// Synchronous to ensure work is done before call is displayed as "answered" CallService.signalingQueue.async {
CallService.signalingQueue.sync {
self.callService.handleAnswerCall(call) self.callService.handleAnswerCall(call)
self.showCall(call)
action.fulfill()
} }
// Signal to the system that the action has been successfully performed.
action.fulfill()
} }
func provider(_ provider: CXProvider, perform action: CXEndCallAction) { public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
Logger.debug("\(TAG) Received \(#function) CXEndCallAction") Logger.debug("\(TAG) Received \(#function) CXEndCallAction")
guard let call = callManager.callWithLocalId(action.callUUID) else { guard let call = callManager.callWithLocalId(action.callUUID) else {
action.fail() action.fail()
@ -212,13 +214,13 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
callManager.removeCall(call) callManager.removeCall(call)
} }
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
Logger.debug("\(TAG) Received \(#function) CXSetHeldCallAction") Logger.debug("\(TAG) Received \(#function) CXSetHeldCallAction")
guard let call = callManager.callWithLocalId(action.callUUID) else { guard let call = callManager.callWithLocalId(action.callUUID) else {
action.fail() action.fail()
return return
} }
Logger.warn("TODO, set held call: \(call)") Logger.warn("TODO, unimplemented set held call: \(call)")
// TODO FIXME // TODO FIXME
// // Update the SpeakerboxCall's underlying hold state. // // Update the SpeakerboxCall's underlying hold state.
@ -235,6 +237,28 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
action.fulfill() action.fulfill()
} }
public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
Logger.debug("\(TAG) Received \(#function) CXSetMutedCallAction")
guard callManager.callWithLocalId(action.callUUID) != nil else {
Logger.error("\(TAG) Failing CXSetMutedCallAction for unknown call: \(action.callUUID)")
action.fail()
return
}
CallService.signalingQueue.async {
self.callService.handleToggledMute(isMuted: action.isMuted)
action.fulfill()
}
}
public func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
Logger.warn("\(TAG) unimplemented \(#function) for CXSetGroupCallAction")
}
public func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
Logger.warn("\(TAG) unimplemented \(#function) for CXPlayDTMFCallAction")
}
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
Logger.debug("\(TAG) Timed out \(#function)") Logger.debug("\(TAG) Timed out \(#function)")

@ -14,6 +14,7 @@ protocol CallUIAdaptee {
func answerCall(_ call: SignalCall) func answerCall(_ call: SignalCall)
func declineCall(_ call: SignalCall) func declineCall(_ call: SignalCall)
func endCall(_ call: SignalCall) func endCall(_ call: SignalCall)
func toggleMute(call: SignalCall, isMuted: Bool)
} }
// Shared default implementations // Shared default implementations
@ -43,7 +44,7 @@ class CallUIAdapter {
if Platform.isSimulator { if Platform.isSimulator {
// CallKit doesn't seem entirely supported in simulator. // CallKit doesn't seem entirely supported in simulator.
// e.g. you can't receive calls in the call screen. // e.g. you can't receive calls in the call screen.
// So we use the non-call kit call UI. // So we use the non-CallKit call UI.
Logger.info("\(TAG) choosing non-callkit adaptee for simulator.") Logger.info("\(TAG) choosing non-callkit adaptee for simulator.")
adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter) adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter)
} else if #available(iOS 10.0, *) { } else if #available(iOS 10.0, *) {
@ -86,4 +87,8 @@ class CallUIAdapter {
internal func showCall(_ call: SignalCall) { internal func showCall(_ call: SignalCall) {
adaptee.showCall(call) adaptee.showCall(call)
} }
internal func toggleMute(call: SignalCall, isMuted: Bool) {
adaptee.toggleMute(call: call, isMuted: isMuted)
}
} }

@ -5,6 +5,7 @@ import Foundation
import WebRTC import WebRTC
import PromiseKit import PromiseKit
// TODO move this somewhere else.
@objc class CallAudioService: NSObject { @objc class CallAudioService: NSObject {
private let TAG = "[CallAudioService]" private let TAG = "[CallAudioService]"
private var vibrateTimer: Timer? private var vibrateTimer: Timer?
@ -126,7 +127,7 @@ import PromiseKit
} }
@objc(OWSCallViewController) @objc(OWSCallViewController)
class CallViewController: UIViewController { class CallViewController: UIViewController, CallDelegate {
enum CallDirection { enum CallDirection {
case unspecified, outgoing, incoming case unspecified, outgoing, incoming
@ -135,7 +136,7 @@ class CallViewController: UIViewController {
let TAG = "[CallViewController]" let TAG = "[CallViewController]"
// Dependencies // Dependencies
let callService: CallService
let callUIAdapter: CallUIAdapter let callUIAdapter: CallUIAdapter
let contactsManager: OWSContactsManager let contactsManager: OWSContactsManager
let audioService: CallAudioService let audioService: CallAudioService
@ -165,7 +166,7 @@ class CallViewController: UIViewController {
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
contactsManager = Environment.getCurrent().contactsManager contactsManager = Environment.getCurrent().contactsManager
callService = Environment.getCurrent().callService let callService = Environment.getCurrent().callService!
callUIAdapter = callService.callUIAdapter callUIAdapter = callService.callUIAdapter
audioService = CallAudioService() audioService = CallAudioService()
super.init(coder: aDecoder) super.init(coder: aDecoder)
@ -173,7 +174,7 @@ class CallViewController: UIViewController {
required init() { required init() {
contactsManager = Environment.getCurrent().contactsManager contactsManager = Environment.getCurrent().contactsManager
callService = Environment.getCurrent().callService let callService = Environment.getCurrent().callService!
callUIAdapter = callService.callUIAdapter callUIAdapter = callService.callUIAdapter
audioService = CallAudioService() audioService = CallAudioService()
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -201,8 +202,8 @@ class CallViewController: UIViewController {
// No-op, since call service is already set up at this point, the result of which was presenting this viewController. // No-op, since call service is already set up at this point, the result of which was presenting this viewController.
} }
call.stateDidChange = callStateDidChange call.delegate = self
callStateDidChange(call.state) stateDidChange(call: call, state: call.state)
} }
// objc accessible way to set our swift enum. // objc accessible way to set our swift enum.
@ -266,13 +267,6 @@ class CallViewController: UIViewController {
// MARK: - Actions // MARK: - Actions
func callStateDidChange(_ newState: CallState) {
DispatchQueue.main.async {
self.updateCallUI(callState: newState)
}
self.audioService.handleState(newState)
}
/** /**
* Ends a connected call. Do not confuse with `didPressDeclineCall`. * Ends a connected call. Do not confuse with `didPressDeclineCall`.
*/ */
@ -290,8 +284,10 @@ class CallViewController: UIViewController {
@IBAction func didPressMute(sender muteButton: UIButton) { @IBAction func didPressMute(sender muteButton: UIButton) {
Logger.info("\(TAG) called \(#function)") Logger.info("\(TAG) called \(#function)")
muteButton.isSelected = !muteButton.isSelected muteButton.isSelected = !muteButton.isSelected
CallService.signalingQueue.async { if let call = self.call {
self.callService.handleToggledMute(isMuted: muteButton.isSelected) callUIAdapter.toggleMute(call: call, isMuted: muteButton.isSelected)
} else {
Logger.warn("\(TAG) pressed mute, but call was unexpectedly nil")
} }
} }
@ -313,9 +309,7 @@ class CallViewController: UIViewController {
return return
} }
CallService.signalingQueue.async { callUIAdapter.answerCall(call)
self.callService.handleAnswerCall(call)
}
} }
/** /**
@ -332,4 +326,19 @@ class CallViewController: UIViewController {
self.dismiss(animated: true) self.dismiss(animated: true)
} }
// MARK: - Call Delegate
internal func stateDidChange(call: SignalCall, state: CallState) {
DispatchQueue.main.async {
self.updateCallUI(callState: state)
}
self.audioService.handleState(state)
}
internal func muteDidChange(call: SignalCall, isMuted: Bool) {
DispatchQueue.main.async {
self.muteButton.isSelected = call.isMuted
}
}
} }

Loading…
Cancel
Save