// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation import PromiseKit import WebRTC import SignalServiceKit import SignalMessaging /** * `CallService` is a global singleton that manages the state of WebRTC-backed Signal Calls * (as opposed to legacy "RedPhone Calls"). * * It serves as a connection between the `CallUIAdapter` and 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. * * Signaling state is synchronized on the main thread and only mutated in the handleXXX family of methods. * * Following is a high level process of the exchange of messages that takes place during call signaling. * * ### 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 | * +----------------------------+-------------------------+ * Start outgoing call: `handleOutgoingCall`... --[SS.CallOffer]--> * ...and start generating ICE updates. * As ICE candidates are generated, `handleLocalAddedIceCandidate` is called. * and we *store* the ICE updates for later. * * Received call offer: `handleReceivedOffer` * Send call answer * <--[SS.CallAnswer]-- * Start generating ICE updates. * As they are generated `handleLocalAddedIceCandidate` is called which immediately sends the ICE updates to the Caller. * <--[SS.ICEUpdate]-- (sent multiple times) * * Received CallAnswer: `handleReceivedAnswer` * So send any stored ice updates (and send future ones immediately) * --[SS.ICEUpdates]--> * * Once compatible ICE updates have been exchanged... * both parties: `handleIceConnected` * * Show remote ringing UI * Connect to offered Data Channel * Show incoming call UI. * * 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]--> */ public enum CallError: Error { case providerReset case assertionError(description: String) case disconnected case externalError(underlyingError: Error) case timeout(description: String) case obsoleteCall(description: String) case fatalError(description: String) } // Should be roughly synced with Android client for consistency private let connectingTimeoutSeconds: TimeInterval = 120 // All Observer methods will be invoked from the main thread. protocol CallServiceObserver: class { /** * Fired whenever the call changes. */ func didUpdateCall(call: SignalCall?) /** * Fired whenever the local or remote video track become active or inactive. */ func didUpdateVideoTracks(call: SignalCall?, localCaptureSession: AVCaptureSession?, remoteVideoTrack: RTCVideoTrack?) } // Gather all per-call state in one place. private class SignalCallData: NSObject { public let call: SignalCall // Used to coordinate promises across delegate methods let fulfillCallConnectedPromise: (() -> Void) let rejectCallConnectedPromise: ((Error) -> Void) let callConnectedPromise: Promise // Used to ensure any received ICE messages wait until the peer connection client is set up. let fulfillPeerConnectionClientPromise: (() -> Void) let rejectPeerConnectionClientPromise: ((Error) -> Void) let peerConnectionClientPromise: Promise // Used to ensure CallOffer was sent before sending any ICE updates. let fulfillReadyToSendIceUpdatesPromise: (() -> Void) let rejectReadyToSendIceUpdatesPromise: ((Error) -> Void) let readyToSendIceUpdatesPromise: Promise weak var localCaptureSession: AVCaptureSession? { didSet { SwiftAssertIsOnMainThread(#function) Logger.info("\(self.logTag) \(#function)") } } weak var remoteVideoTrack: RTCVideoTrack? { didSet { SwiftAssertIsOnMainThread(#function) Logger.info("\(self.logTag) \(#function)") } } var isRemoteVideoEnabled = false { didSet { SwiftAssertIsOnMainThread(#function) Logger.info("\(self.logTag) \(#function): \(isRemoteVideoEnabled)") } } var peerConnectionClient: PeerConnectionClient? { didSet { SwiftAssertIsOnMainThread(#function) Logger.debug("\(self.logTag) .peerConnectionClient setter: \(oldValue != nil) -> \(peerConnectionClient != nil) \(String(describing: peerConnectionClient))") } } required init(call: SignalCall) { self.call = call let (callConnectedPromise, fulfillCallConnectedPromise, rejectCallConnectedPromise) = Promise.pending() self.callConnectedPromise = callConnectedPromise self.fulfillCallConnectedPromise = { fulfillCallConnectedPromise(()) } self.rejectCallConnectedPromise = rejectCallConnectedPromise let (peerConnectionClientPromise, fulfillPeerConnectionClientPromise, rejectPeerConnectionClientPromise) = Promise.pending() self.peerConnectionClientPromise = peerConnectionClientPromise self.fulfillPeerConnectionClientPromise = { fulfillPeerConnectionClientPromise(()) } self.rejectPeerConnectionClientPromise = rejectPeerConnectionClientPromise let (readyToSendIceUpdatesPromise, fulfillReadyToSendIceUpdatesPromise, rejectReadyToSendIceUpdatesPromise) = Promise.pending() self.readyToSendIceUpdatesPromise = readyToSendIceUpdatesPromise self.fulfillReadyToSendIceUpdatesPromise = { fulfillReadyToSendIceUpdatesPromise(()) } self.rejectReadyToSendIceUpdatesPromise = rejectReadyToSendIceUpdatesPromise super.init() } deinit { Logger.debug("[SignalCallData] deinit") } // MARK: - public func terminate() { SwiftAssertIsOnMainThread(#function) Logger.debug("\(self.logTag) in \(#function)") self.call.removeAllObservers() // In case we're still waiting on this promise somewhere, we need to reject it to avoid a memory leak. // There is no harm in rejecting a previously fulfilled promise. rejectCallConnectedPromise(CallError.obsoleteCall(description: "Terminating call")) // In case we're still waiting on the peer connection setup somewhere, we need to reject it to avoid a memory leak. // There is no harm in rejecting a previously fulfilled promise. rejectPeerConnectionClientPromise(CallError.obsoleteCall(description: "Terminating call")) // In case we're still waiting on this promise somewhere, we need to reject it to avoid a memory leak. // There is no harm in rejecting a previously fulfilled promise. rejectReadyToSendIceUpdatesPromise(CallError.obsoleteCall(description: "Terminating call")) peerConnectionClient?.terminate() Logger.debug("\(self.logTag) setting peerConnectionClient in \(#function)") } } // This class' state should only be accessed on the main queue. @objc public class CallService: NSObject, CallObserver, PeerConnectionClientDelegate { // MARK: - Properties var observers = [Weak]() // MARK: Dependencies private let accountManager: AccountManager private let messageSender: MessageSender private let contactsManager: OWSContactsManager private let primaryStorage: OWSPrimaryStorage // Exposed by environment.m internal let notificationsAdapter: CallNotificationsAdapter @objc public var callUIAdapter: CallUIAdapter! // MARK: Class static let fallbackIceServer = RTCIceServer(urlStrings: ["stun:stun1.l.google.com:19302"]) // MARK: Ivars fileprivate var callData: SignalCallData? { didSet { SwiftAssertIsOnMainThread(#function) oldValue?.call.removeObserver(self) callData?.call.addObserverAndSyncState(observer: self) updateIsVideoEnabled() // Prevent device from sleeping while we have an active call. if oldValue != callData { if let oldValue = oldValue { DeviceSleepManager.sharedInstance.removeBlock(blockObject: oldValue) } if let callData = callData { DeviceSleepManager.sharedInstance.addBlock(blockObject: callData) self.startCallTimer() } else { stopAnyCallTimer() } } Logger.debug("\(self.logTag) .callData setter: \(oldValue?.call.identifiersForLogs as Optional) -> \(callData?.call.identifiersForLogs as Optional)") for observer in observers { observer.value?.didUpdateCall(call: callData?.call) } } } @objc var call: SignalCall? { get { SwiftAssertIsOnMainThread(#function) return callData?.call } } var peerConnectionClient: PeerConnectionClient? { get { SwiftAssertIsOnMainThread(#function) return callData?.peerConnectionClient } } weak var localCaptureSession: AVCaptureSession? { get { SwiftAssertIsOnMainThread(#function) return callData?.localCaptureSession } } var remoteVideoTrack: RTCVideoTrack? { get { SwiftAssertIsOnMainThread(#function) return callData?.remoteVideoTrack } } var isRemoteVideoEnabled: Bool { get { SwiftAssertIsOnMainThread(#function) guard let callData = callData else { return false } return callData.isRemoteVideoEnabled } } @objc public required init(accountManager: AccountManager, contactsManager: OWSContactsManager, messageSender: MessageSender, notificationsAdapter: CallNotificationsAdapter) { self.accountManager = accountManager self.contactsManager = contactsManager self.messageSender = messageSender self.notificationsAdapter = notificationsAdapter self.primaryStorage = OWSPrimaryStorage.shared() super.init() SwiftSingletons.register(self) self.createCallUIAdapter() NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: NSNotification.Name.OWSApplicationDidEnterBackground, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.OWSApplicationDidBecomeActive, object: nil) } deinit { NotificationCenter.default.removeObserver(self) } @objc func didEnterBackground() { SwiftAssertIsOnMainThread(#function) self.updateIsVideoEnabled() } @objc func didBecomeActive() { SwiftAssertIsOnMainThread(#function) self.updateIsVideoEnabled() } /** * Choose whether to use CallKit or a Notification backed interface for calling. */ @objc public func createCallUIAdapter() { SwiftAssertIsOnMainThread(#function) if self.call != nil { Logger.warn("\(self.logTag) ending current call in \(#function). Did user toggle callkit preference while in a call?") self.terminateCall() } self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationsAdapter: self.notificationsAdapter) } // MARK: - Service Actions /** * Initiate an outgoing call. */ func handleOutgoingCall(_ call: SignalCall) -> Promise { SwiftAssertIsOnMainThread(#function) guard self.call == nil else { let errorDescription = "\(self.logTag) call was unexpectedly already set." Logger.error(errorDescription) call.state = .localFailure OWSProdError(OWSAnalyticsEvents.callServiceCallAlreadySet(), file: #file, function: #function, line: #line) return Promise(error: CallError.assertionError(description: errorDescription)) } let callData = SignalCallData(call: call) self.callData = callData let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: call.remotePhoneNumber, callType: RPRecentCallTypeOutgoingIncomplete, in: call.thread) callRecord.save() call.callRecord = callRecord let promise = getIceServers().then { iceServers -> Promise in Logger.debug("\(self.logTag) got ice servers:\(iceServers) for call: \(call.identifiersForLogs)") guard self.call == call else { throw CallError.obsoleteCall(description: "obsolete call in \(#function)") } guard callData.peerConnectionClient == nil else { let errorDescription = "\(self.logTag) peerconnection was unexpectedly already set." Logger.error(errorDescription) OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionAlreadySet(), file: #file, function: #function, line: #line) throw CallError.assertionError(description: errorDescription) } let useTurnOnly = Environment.current().preferences.doCallsHideIPAddress() let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .outgoing, useTurnOnly: useTurnOnly) Logger.debug("\(self.logTag) setting peerConnectionClient in \(#function) for call: \(call.identifiersForLogs)") callData.peerConnectionClient = peerConnectionClient callData.fulfillPeerConnectionClientPromise() return peerConnectionClient.createOffer() }.then { (sessionDescription: HardenedRTCSessionDescription) -> Promise in guard self.call == call else { throw CallError.obsoleteCall(description: "obsolete call in \(#function)") } guard let peerConnectionClient = self.peerConnectionClient else { owsFail("Missing peerConnectionClient in \(#function)") throw CallError.obsoleteCall(description: "Missing peerConnectionClient in \(#function)") } return peerConnectionClient.setLocalSessionDescription(sessionDescription).then { do { let offerBuilder = SSKProtoCallMessageOffer.SSKProtoCallMessageOfferBuilder(id: call.signalingId, sessionDescription: sessionDescription.sdp) let callMessage = OWSOutgoingCallMessage(thread: call.thread, offerMessage: try offerBuilder.build()) return self.messageSender.sendPromise(message: callMessage) } catch { owsFail("Couldn't build proto in \(#function)") throw CallError.fatalError(description: "Couldn't build proto in \(#function)") } } }.then { guard self.call == call else { throw CallError.obsoleteCall(description: "obsolete call in \(#function)") } // For outgoing calls, wait until call offer is sent before we send any ICE updates, to ensure message ordering for // clients that don't support receiving ICE updates before receiving the call offer. self.readyToSendIceUpdates(call: call) // Don't let the outgoing call ring forever. We don't support inbound ringing forever anyway. let timeout: Promise = after(interval: connectingTimeoutSeconds).then { () -> Void in // This code will always be called, whether or not the call has timed out. // However, if the call has already connected, the `race` promise will have already been // fulfilled. Rejecting an already fulfilled promise is a no-op. throw CallError.timeout(description: "timed out waiting to receive call answer") } return race(timeout, callData.callConnectedPromise) }.then { Logger.info(self.call == call ? "\(self.logTag) outgoing call connected: \(call.identifiersForLogs)." : "\(self.logTag) obsolete outgoing call connected: \(call.identifiersForLogs).") }.catch { error in Logger.error("\(self.logTag) placing call \(call.identifiersForLogs) failed with error: \(error)") if let callError = error as? CallError { if case .timeout = callError { OWSProdInfo(OWSAnalyticsEvents.callServiceErrorTimeoutWhileConnectingOutgoing(), file: #file, function: #function, line: #line) } OWSProdInfo(OWSAnalyticsEvents.callServiceErrorOutgoingConnectionFailedInternal(), file: #file, function: #function, line: #line) self.handleFailedCall(failedCall: call, error: callError) } else { OWSProdInfo(OWSAnalyticsEvents.callServiceErrorOutgoingConnectionFailedExternal(), file: #file, function: #function, line: #line) let externalError = CallError.externalError(underlyingError: error) self.handleFailedCall(failedCall: call, error: externalError) } } promise.retainUntilComplete() return promise } func readyToSendIceUpdates(call: SignalCall) { SwiftAssertIsOnMainThread(#function) guard let callData = self.callData else { self.handleFailedCall(failedCall: call, error: .obsoleteCall(description:"obsolete call in \(#function)")) return } guard callData.call == call else { Logger.warn("\(self.logTag) ignoring \(#function) for call other than current call") return } callData.fulfillReadyToSendIceUpdatesPromise() } /** * Called by the call initiator after receiving a CallAnswer from the callee. */ public func handleReceivedAnswer(thread: TSContactThread, callId: UInt64, sessionDescription: String) { Logger.info("\(self.logTag) received call answer for call: \(callId) thread: \(thread.contactIdentifier())") SwiftAssertIsOnMainThread(#function) guard let call = self.call else { Logger.warn("\(self.logTag) ignoring obsolete call: \(callId) in \(#function)") return } guard call.signalingId == callId else { Logger.warn("\(self.logTag) ignoring mismatched call: \(callId) currentCall: \(call.signalingId) in \(#function)") return } guard let peerConnectionClient = self.peerConnectionClient else { OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "peerConnectionClient was unexpectedly nil in \(#function)")) return } let sessionDescription = RTCSessionDescription(type: .answer, sdp: sessionDescription) let setDescriptionPromise = peerConnectionClient.setRemoteSessionDescription(sessionDescription).then { Logger.debug("\(self.logTag) successfully set remote description") }.catch { error in if let callError = error as? CallError { OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleReceivedErrorInternal(), file: #file, function: #function, line: #line) self.handleFailedCall(failedCall: call, error: callError) } else { OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleReceivedErrorExternal(), file: #file, function: #function, line: #line) let externalError = CallError.externalError(underlyingError: error) self.handleFailedCall(failedCall: call, error: externalError) } } setDescriptionPromise.retainUntilComplete() } /** * User didn't answer incoming call */ public func handleMissedCall(_ call: SignalCall) { SwiftAssertIsOnMainThread(#function) // Insert missed call record if let callRecord = call.callRecord { if callRecord.callType == RPRecentCallTypeIncoming { callRecord.updateCallType(RPRecentCallTypeIncomingMissed) } } else { call.callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: call.thread.contactIdentifier(), callType: RPRecentCallTypeIncomingMissed, in: call.thread) } assert(call.callRecord != nil) call.callRecord?.save() self.callUIAdapter.reportMissedCall(call) } /** * Received a call while already in another call. */ private func handleLocalBusyCall(_ call: SignalCall) { Logger.info("\(self.logTag) \(#function) for call: \(call.identifiersForLogs) thread: \(call.thread.contactIdentifier())") SwiftAssertIsOnMainThread(#function) do { let busyBuilder = SSKProtoCallMessageBusy.SSKProtoCallMessageBusyBuilder(id: call.signalingId) let callMessage = OWSOutgoingCallMessage(thread: call.thread, busyMessage: try busyBuilder.build()) let sendPromise = messageSender.sendPromise(message: callMessage) sendPromise.retainUntilComplete() handleMissedCall(call) } catch { owsFail("Couldn't build proto in \(#function)") } } /** * The callee was already in another call. */ public func handleRemoteBusy(thread: TSContactThread, callId: UInt64) { Logger.info("\(self.logTag) \(#function) for thread: \(thread.contactIdentifier())") SwiftAssertIsOnMainThread(#function) guard let call = self.call else { Logger.warn("\(self.logTag) ignoring obsolete call: \(callId) in \(#function)") return } guard call.signalingId == callId else { Logger.warn("\(self.logTag) ignoring mismatched call: \(callId) currentCall: \(call.signalingId) in \(#function)") return } guard thread.contactIdentifier() == call.remotePhoneNumber else { Logger.warn("\(self.logTag) ignoring obsolete call in \(#function)") return } call.state = .remoteBusy callUIAdapter.remoteBusy(call) terminateCall() } /** * 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) { SwiftAssertIsOnMainThread(#function) let newCall = SignalCall.incomingCall(localId: UUID(), remotePhoneNumber: thread.contactIdentifier(), signalingId: callId) Logger.info("\(self.logTag) receivedCallOffer: \(newCall.identifiersForLogs)") let untrustedIdentity = OWSIdentityManager.shared().untrustedIdentityForSending(toRecipientId: thread.contactIdentifier()) guard untrustedIdentity == nil else { Logger.warn("\(self.logTag) missed a call due to untrusted identity: \(newCall.identifiersForLogs)") let callerName = self.contactsManager.displayName(forPhoneIdentifier: thread.contactIdentifier()) switch untrustedIdentity!.verificationState { case .verified: owsFail("\(self.logTag) shouldn't have missed a call due to untrusted identity if the identity is verified") self.notificationsAdapter.presentMissedCall(newCall, callerName: callerName) case .default: self.notificationsAdapter.presentMissedCallBecauseOfNewIdentity(call: newCall, callerName: callerName) case .noLongerVerified: self.notificationsAdapter.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: newCall, callerName: callerName) } let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: thread.contactIdentifier(), callType: RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity, in: thread) assert(newCall.callRecord == nil) newCall.callRecord = callRecord callRecord.save() terminateCall() return } guard self.call == nil else { let existingCall = self.call! // TODO on iOS10+ we can use CallKit to swap calls rather than just returning busy immediately. Logger.info("\(self.logTag) receivedCallOffer: \(newCall.identifiersForLogs) but we're already in call: \(existingCall.identifiersForLogs)") handleLocalBusyCall(newCall) if existingCall.remotePhoneNumber == newCall.remotePhoneNumber { Logger.info("\(self.logTag) handling call from current call user as remote busy.: \(newCall.identifiersForLogs) but we're already in call: \(existingCall.identifiersForLogs)") // If we're receiving a new call offer from the user we already think we have a call with, // terminate our current call to get back to a known good state. If they call back, we'll // be ready. // // TODO: Auto-accept this incoming call if our current call was either a) outgoing or // b) never connected. There will be a bit of complexity around making sure that two // parties that call each other at the same time end up connected. switch existingCall.state { case .idle, .dialing, .remoteRinging: // If both users are trying to call each other at the same time, // both should see busy. handleRemoteBusy(thread: existingCall.thread, callId: existingCall.signalingId) case .answering, .localRinging, .connected, .localFailure, .localHangup, .remoteHangup, .remoteBusy, .reconnecting: // If one user calls another while the other has a "vestigial" call with // that same user, fail the old call. terminateCall() } } return } Logger.info("\(self.logTag) starting new call: \(newCall.identifiersForLogs)") let callData = SignalCallData(call: newCall) self.callData = callData var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in SwiftAssertIsOnMainThread(#function) guard status == .expired else { return } guard let strongSelf = self else { return } let timeout = CallError.timeout(description: "background task time ran out before call connected.") guard strongSelf.call == newCall else { Logger.warn("\(strongSelf.logTag) ignoring obsolete call in \(#function)") return } strongSelf.handleFailedCall(failedCall: newCall, error: timeout) }) let incomingCallPromise = firstly { return getIceServers() }.then { (iceServers: [RTCIceServer]) -> Promise in // FIXME for first time call recipients I think we'll see mic/camera permission requests here, // even though, from the users perspective, no incoming call is yet visible. guard self.call == newCall else { throw CallError.obsoleteCall(description: "getIceServers() response for obsolete call") } assert(self.peerConnectionClient == nil, "Unexpected PeerConnectionClient instance") // For contacts not stored in our system contacts, we assume they are an unknown caller, and we force // a TURN connection, so as not to reveal any connectivity information (IP/port) to the caller. let isUnknownCaller = !self.contactsManager.hasSignalAccount(forRecipientId: thread.contactIdentifier()) let useTurnOnly = isUnknownCaller || Environment.current().preferences.doCallsHideIPAddress() Logger.debug("\(self.logTag) setting peerConnectionClient in \(#function) for: \(newCall.identifiersForLogs)") let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .incoming, useTurnOnly: useTurnOnly) callData.peerConnectionClient = peerConnectionClient callData.fulfillPeerConnectionClientPromise() let offerSessionDescription = RTCSessionDescription(type: .offer, sdp: callerSessionDescription) let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) // Find a sessionDescription compatible with my constraints and the remote sessionDescription return peerConnectionClient.negotiateSessionDescription(remoteDescription: offerSessionDescription, constraints: constraints) }.then { (negotiatedSessionDescription: HardenedRTCSessionDescription) in Logger.debug("\(self.logTag) set the remote description for: \(newCall.identifiersForLogs)") guard self.call == newCall else { throw CallError.obsoleteCall(description: "negotiateSessionDescription() response for obsolete call") } do { let answerBuilder = SSKProtoCallMessageAnswer.SSKProtoCallMessageAnswerBuilder(id: newCall.signalingId, sessionDescription: negotiatedSessionDescription.sdp) let callAnswerMessage = OWSOutgoingCallMessage(thread: thread, answerMessage: try answerBuilder.build()) return self.messageSender.sendPromise(message: callAnswerMessage) } catch { owsFail("Couldn't build proto in \(#function)") throw CallError.fatalError(description: "Couldn't build proto in \(#function)") } }.then { guard self.call == newCall else { throw CallError.obsoleteCall(description: "sendPromise(message: ) response for obsolete call") } Logger.debug("\(self.logTag) successfully sent callAnswerMessage for: \(newCall.identifiersForLogs)") // There's nothing technically forbidding receiving ICE updates before receiving the CallAnswer, but this // a more intuitive ordering. self.readyToSendIceUpdates(call: newCall) let timeout: Promise = after(interval: connectingTimeoutSeconds).then { () -> Void in // rejecting a promise by throwing is safely a no-op if the promise has already been fulfilled OWSProdInfo(OWSAnalyticsEvents.callServiceErrorTimeoutWhileConnectingIncoming(), file: #file, function: #function, line: #line) throw CallError.timeout(description: "timed out waiting for call to connect") } // This will be fulfilled (potentially) by the RTCDataChannel delegate method return race(callData.callConnectedPromise, timeout) }.then { Logger.info(self.call == newCall ? "\(self.logTag) incoming call connected: \(newCall.identifiersForLogs)." : "\(self.logTag) obsolete incoming call connected: \(newCall.identifiersForLogs).") }.catch { error in guard self.call == newCall else { Logger.debug("\(self.logTag) ignoring error: \(error) for obsolete call: \(newCall.identifiersForLogs).") return } if let callError = error as? CallError { OWSProdInfo(OWSAnalyticsEvents.callServiceErrorIncomingConnectionFailedInternal(), file: #file, function: #function, line: #line) self.handleFailedCall(failedCall: newCall, error: callError) } else { OWSProdInfo(OWSAnalyticsEvents.callServiceErrorIncomingConnectionFailedExternal(), file: #file, function: #function, line: #line) let externalError = CallError.externalError(underlyingError: error) self.handleFailedCall(failedCall: newCall, error: externalError) } }.always { Logger.debug("\(self.logTag) ending background task awaiting inbound call connection") assert(backgroundTask != nil) backgroundTask = nil } incomingCallPromise.retainUntilComplete() } /** * 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) { SwiftAssertIsOnMainThread(#function) Logger.verbose("\(logTag) \(#function) callId: \(callId)") guard let callData = self.callData else { Logger.info("\(logTag) ignoring remote ice update, since there is no current call.") return } callData.peerConnectionClientPromise.then { () -> Void in SwiftAssertIsOnMainThread(#function) guard let call = self.call else { Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current call. Call already ended?") return } guard call.signalingId == callId else { Logger.warn("\(self.logTag) ignoring mismatched call: \(callId) currentCall: \(call.signalingId) in \(#function)") return } guard thread.contactIdentifier() == call.thread.contactIdentifier() else { Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) due to thread mismatch. Call already ended?") return } guard let peerConnectionClient = self.peerConnectionClient else { Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current peerConnectionClient. Call already ended?") return } Logger.verbose("\(self.logTag) \(#function) addRemoteIceCandidate") peerConnectionClient.addRemoteIceCandidate(RTCIceCandidate(sdp: sdp, sdpMLineIndex: lineIndex, sdpMid: mid)) }.catch { error in OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleRemoteAddedIceCandidate(), file: #file, function: #function, line: #line) Logger.error("\(self.logTag) in \(#function) peerConnectionClientPromise failed with error: \(error)") }.retainUntilComplete() } /** * Local client (could be caller or callee) generated some connectivity information that we should send to the * remote client. */ private func handleLocalAddedIceCandidate(_ iceCandidate: RTCIceCandidate) { SwiftAssertIsOnMainThread(#function) guard let callData = self.callData else { OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) self.handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring local ice candidate, since there is no current call.")) return } let call = callData.call // Wait until we've sent the CallOffer before sending any ice updates for the call to ensure // intuitive message ordering for other clients. callData.readyToSendIceUpdatesPromise.then { () -> Void in guard call == self.call else { self.handleFailedCurrentCall(error: .obsoleteCall(description: "current call changed since we became ready to send ice updates")) return } guard call.state != .idle else { // This will only be called for the current peerConnectionClient, so // fail the current call. OWSProdError(OWSAnalyticsEvents.callServiceCallUnexpectedlyIdle(), file: #file, function: #function, line: #line) self.handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring local ice candidate, since call is now idle.")) return } guard let sdpMid = iceCandidate.sdpMid else { owsFail("Missing sdpMid in \(#function)") throw CallError.fatalError(description: "Missing sdpMid in \(#function)") } guard iceCandidate.sdpMLineIndex < UINT32_MAX else { owsFail("Invalid sdpMLineIndex in \(#function)") throw CallError.fatalError(description: "Invalid sdpMLineIndex in \(#function)") } Logger.info("\(self.logTag) in \(#function) sending ICE Candidate \(call.identifiersForLogs).") do { /** * Sent by both parties out of band of the RTC calling channels, as part of setting up those channels. The messages * include network accessibility information from the perspective of each client. Once compatible ICEUpdates have been * exchanged, the clients can connect. */ let iceUpdateBuilder = SSKProtoCallMessageIceUpdate.SSKProtoCallMessageIceUpdateBuilder(id: call.signalingId, sdpMid: sdpMid, sdpMlineIndex: UInt32(iceCandidate.sdpMLineIndex), sdp: iceCandidate.sdp) let callMessage = OWSOutgoingCallMessage(thread: call.thread, iceUpdateMessage: try iceUpdateBuilder.build()) let sendPromise = self.messageSender.sendPromise(message: callMessage) sendPromise.retainUntilComplete() } catch { owsFail("Couldn't build proto in \(#function)") throw CallError.fatalError(description: "Couldn't build proto in \(#function)") } }.catch { error in OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalAddedIceCandidate(), file: #file, function: #function, line: #line) Logger.error("\(self.logTag) in \(#function) waitUntilReadyToSendIceUpdates failed with error: \(error)") }.retainUntilComplete() } /** * 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() { SwiftAssertIsOnMainThread(#function) guard let call = self.call else { // This will only be called for the current peerConnectionClient, so // fail the current call. OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) ignoring \(#function) since there is no current call.")) return } Logger.info("\(self.logTag) in \(#function): \(call.identifiersForLogs).") switch call.state { case .dialing: call.state = .remoteRinging case .answering: call.state = .localRinging self.callUIAdapter.reportIncomingCall(call, thread: call.thread) case .remoteRinging: Logger.info("\(self.logTag) call already ringing. Ignoring \(#function): \(call.identifiersForLogs).") case .connected: Logger.info("\(self.logTag) Call reconnected \(#function): \(call.identifiersForLogs).") case .reconnecting: call.state = .connected case .idle, .localRinging, .localFailure, .localHangup, .remoteHangup, .remoteBusy: owsFail("\(self.logTag) unexpected call state for \(#function): \(call.state): \(call.identifiersForLogs).") } } private func handleIceDisconnected() { SwiftAssertIsOnMainThread(#function) guard let call = self.call else { // This will only be called for the current peerConnectionClient, so // fail the current call. OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) ignoring \(#function) since there is no current call.")) return } Logger.info("\(self.logTag) in \(#function): \(call.identifiersForLogs).") switch call.state { case .remoteRinging, .localRinging: Logger.debug("\(self.logTag) in \(#function) disconnect while ringing... we'll keep ringing") case .connected: call.state = .reconnecting default: owsFail("\(self.logTag) unexpected call state for \(#function): \(call.state): \(call.identifiersForLogs).") } } /** * The remote client (caller or callee) ended the call. */ public func handleRemoteHangup(thread: TSContactThread, callId: UInt64) { Logger.debug("\(self.logTag) in \(#function)") SwiftAssertIsOnMainThread(#function) guard let call = self.call else { // This may happen if we hang up slightly before they hang up. handleFailedCurrentCall(error: .obsoleteCall(description:"\(self.logTag) call was unexpectedly nil in \(#function)")) return } guard call.signalingId == callId else { Logger.warn("\(self.logTag) ignoring mismatched call: \(callId) currentCall: \(call.signalingId) in \(#function)") return } guard thread.contactIdentifier() == call.thread.contactIdentifier() else { // This can safely be ignored. // We don't want to fail the current call because an old call was slow to send us the hangup message. Logger.warn("\(self.logTag) ignoring hangup for thread: \(thread.contactIdentifier()) which is not the current call: \(call.identifiersForLogs)") return } Logger.info("\(self.logTag) in \(#function): \(call.identifiersForLogs).") switch call.state { case .idle, .dialing, .answering, .localRinging, .localFailure, .remoteBusy, .remoteRinging: handleMissedCall(call) case .connected, .reconnecting, .localHangup, .remoteHangup: Logger.info("\(self.logTag) call is finished.") } call.state = .remoteHangup // Notify UI callUIAdapter.remoteDidHangupCall(call) // self.call is nil'd in `terminateCall`, so it's important we update it's state *before* calling `terminateCall` terminateCall() } /** * User chose to answer call referred to by call `localId`. Used by the Callee only. * * Used by notification actions which can't serialize a call object. */ @objc public func handleAnswerCall(localId: UUID) { SwiftAssertIsOnMainThread(#function) guard let call = self.call else { // This should never happen; return to a known good state. owsFail("\(self.logTag) call was unexpectedly nil in \(#function)") OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) call was unexpectedly nil in \(#function)")) return } guard call.localId == localId else { // This should never happen; return to a known good state. owsFail("\(self.logTag) callLocalId:\(localId) doesn't match current calls: \(call.localId)") OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) callLocalId:\(localId) doesn't match current calls: \(call.localId)")) return } self.handleAnswerCall(call) } /** * User chose to answer call referred to by call `localId`. Used by the Callee only. */ public func handleAnswerCall(_ call: SignalCall) { SwiftAssertIsOnMainThread(#function) Logger.debug("\(self.logTag) in \(#function)") guard let currentCallData = self.callData else { OWSProdError(OWSAnalyticsEvents.callServiceCallDataMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "\(self.logTag) callData unexpectedly nil in \(#function)")) return } guard call == currentCallData.call else { // This could conceivably happen if the other party of an old call was slow to send us their answer // and we've subsequently engaged in another call. Don't kill the current call, but just ignore it. Logger.warn("\(self.logTag) ignoring \(#function) for call other than current call") return } guard let peerConnectionClient = self.peerConnectionClient else { OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "\(self.logTag) missing peerconnection client in \(#function)")) return } Logger.info("\(self.logTag) in \(#function): \(call.identifiersForLogs).") let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: call.remotePhoneNumber, callType: RPRecentCallTypeIncomingIncomplete, in: call.thread) callRecord.save() call.callRecord = callRecord var messageData: Data do { let connectedBuilder = WebRTCProtoConnected.WebRTCProtoConnectedBuilder(id: call.signalingId) let dataBuilder = WebRTCProtoData.WebRTCProtoDataBuilder() dataBuilder.setConnected(try connectedBuilder.build()) messageData = try dataBuilder.buildSerializedData() } catch { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "\(self.logTag) couldn't build proto in \(#function)")) return } peerConnectionClient.sendDataChannelMessage(data: messageData, description: "connected", isCritical: true) handleConnectedCall(currentCallData) } /** * 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. */ private func handleConnectedCall(_ callData: SignalCallData) { Logger.info("\(self.logTag) in \(#function)") SwiftAssertIsOnMainThread(#function) guard let peerConnectionClient = callData.peerConnectionClient else { OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "\(self.logTag) peerConnectionClient unexpectedly nil in \(#function)")) return } Logger.info("\(self.logTag) handleConnectedCall: \(callData.call.identifiersForLogs).") // cancel connection timeout callData.fulfillCallConnectedPromise() callData.call.state = .connected // We don't risk transmitting any media until the remote client has admitted to being connected. ensureAudioState(call: callData.call, peerConnectionClient: peerConnectionClient) peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) } /** * 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) { SwiftAssertIsOnMainThread(#function) guard let call = self.call else { // This should never happen; return to a known good state. owsFail("\(self.logTag) call was unexpectedly nil in \(#function)") OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) call was unexpectedly nil in \(#function)")) return } guard call.localId == localId else { // This should never happen; return to a known good state. owsFail("\(self.logTag) callLocalId:\(localId) doesn't match current calls: \(call.localId)") OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) callLocalId:\(localId) doesn't match current calls: \(call.localId)")) return } self.handleDeclineCall(call) } /** * Local user chose to decline the call vs. answering it. * * Incoming call only. */ public func handleDeclineCall(_ call: SignalCall) { SwiftAssertIsOnMainThread(#function) Logger.info("\(self.logTag) in \(#function): \(call.identifiersForLogs).") if let callRecord = call.callRecord { owsFail("Not expecting callrecord to already be set") callRecord.updateCallType(RPRecentCallTypeIncomingDeclined) } else { let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: call.remotePhoneNumber, callType: RPRecentCallTypeIncomingDeclined, in: call.thread) callRecord.save() call.callRecord = callRecord } // Currently we just handle this as a hangup. But we could offer more descriptive action. e.g. DataChannel message handleLocalHungupCall(call) } /** * Local user chose to end the call. * * Can be used for Incoming and Outgoing calls. */ func handleLocalHungupCall(_ call: SignalCall) { SwiftAssertIsOnMainThread(#function) guard let currentCall = self.call else { Logger.info("\(self.logTag) in \(#function), but no current call. Other party hung up just before us.") // terminating the call might be redundant, but it shouldn't hurt. terminateCall() return } guard call == currentCall else { OWSProdError(OWSAnalyticsEvents.callServiceCallMismatch(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "\(self.logTag) ignoring \(#function) for call other than current call")) return } Logger.info("\(self.logTag) in \(#function): \(call.identifiersForLogs).") call.state = .localHangup if let callRecord = call.callRecord { if callRecord.callType == RPRecentCallTypeOutgoingIncomplete { callRecord.updateCallType(RPRecentCallTypeOutgoingMissed) } } else { owsFail("\(self.logTag) missing call record in \(#function)") } // TODO something like this lifted from Signal-Android. // this.accountManager.cancelInFlightRequests(); // this.messageSender.cancelInFlightRequests(); if let peerConnectionClient = self.peerConnectionClient { // Stop audio capture ASAP ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) // If the call is connected, we can send the hangup via the data channel for faster hangup. var messageData: Data do { let hangupBuilder = WebRTCProtoHangup.WebRTCProtoHangupBuilder(id: call.signalingId) let dataBuilder = WebRTCProtoData.WebRTCProtoDataBuilder() dataBuilder.setHangup(try hangupBuilder.build()) messageData = try dataBuilder.buildSerializedData() } catch { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "\(self.logTag) couldn't build proto in \(#function)")) return } peerConnectionClient.sendDataChannelMessage(data: messageData, description: "hangup", isCritical: true) } else { Logger.info("\(self.logTag) ending call before peer connection created. Device offline or quick hangup.") } // If the call hasn't started yet, we don't have a data channel to communicate the hang up. Use Signal Service Message. do { let hangupBuilder = SSKProtoCallMessageHangup.SSKProtoCallMessageHangupBuilder(id: call.signalingId) let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) let sendPromise = self.messageSender.sendPromise(message: callMessage).then { Logger.debug("\(self.logTag) successfully sent hangup call message to \(call.thread.contactIdentifier())") }.catch { error in OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalHungupCall(), file: #file, function: #function, line: #line) Logger.error("\(self.logTag) failed to send hangup call message to \(call.thread.contactIdentifier()) with error: \(error)") } sendPromise.retainUntilComplete() terminateCall() } catch { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "\(self.logTag) couldn't build proto in \(#function)")) } } /** * Local user toggled to mute audio. * * Can be used for Incoming and Outgoing calls. */ func setIsMuted(call: SignalCall, isMuted: Bool) { SwiftAssertIsOnMainThread(#function) guard call == self.call else { // This can happen after a call has ended. Reproducible on iOS11, when the other party ends the call. Logger.info("\(self.logTag) ignoring mute request for obsolete call") return } call.isMuted = isMuted guard let peerConnectionClient = self.peerConnectionClient else { // The peer connection might not be created yet. return } ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) } /** * Local user toggled to hold call. Currently only possible via CallKit screen, * e.g. when another Call comes in. */ func setIsOnHold(call: SignalCall, isOnHold: Bool) { SwiftAssertIsOnMainThread(#function) guard call == self.call else { Logger.info("\(self.logTag) ignoring held request for obsolete call") return } call.isOnHold = isOnHold guard let peerConnectionClient = self.peerConnectionClient else { // The peer connection might not be created yet. return } ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) } func ensureAudioState(call: SignalCall, peerConnectionClient: PeerConnectionClient) { guard call.state == .connected else { peerConnectionClient.setAudioEnabled(enabled: false) return } guard !call.isMuted else { peerConnectionClient.setAudioEnabled(enabled: false) return } guard !call.isOnHold else { peerConnectionClient.setAudioEnabled(enabled: false) return } peerConnectionClient.setAudioEnabled(enabled: true) } /** * Local user toggled video. * * Can be used for Incoming and Outgoing calls. */ func setHasLocalVideo(hasLocalVideo: Bool) { SwiftAssertIsOnMainThread(#function) guard let frontmostViewController = UIApplication.shared.frontmostViewController else { owsFail("\(self.logTag) could not identify frontmostViewController in \(#function)") return } frontmostViewController.ows_ask(forCameraPermissions: { [weak self] granted in guard let strongSelf = self else { return } if (granted) { // Success callback; camera permissions are granted. strongSelf.setHasLocalVideoWithCameraPermissions(hasLocalVideo: hasLocalVideo) } else { // Failed callback; camera permissions are _NOT_ granted. // We don't need to worry about the user granting or remoting this permission // during a call while the app is in the background, because changing this // permission kills the app. OWSAlerts.showAlert(title: NSLocalizedString("MISSING_CAMERA_PERMISSION_TITLE", comment: "Alert title when camera is not authorized"), message: NSLocalizedString("MISSING_CAMERA_PERMISSION_MESSAGE", comment: "Alert body when camera is not authorized")) } }) } private func setHasLocalVideoWithCameraPermissions(hasLocalVideo: Bool) { SwiftAssertIsOnMainThread(#function) guard let call = self.call else { // This can happen if you toggle local video right after // the other user ends the call. Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete call") return } call.hasLocalVideo = hasLocalVideo guard let peerConnectionClient = self.peerConnectionClient else { // The peer connection might not be created yet. return } if call.state == .connected { peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) } } @objc func handleCallKitStartVideo() { SwiftAssertIsOnMainThread(#function) self.setHasLocalVideo(hasLocalVideo: true) } func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) { SwiftAssertIsOnMainThread(#function) guard let peerConnectionClient = self.peerConnectionClient else { return } peerConnectionClient.setCameraSource(isUsingFrontCamera: isUsingFrontCamera) } /** * 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: WebRTCProtoData) { SwiftAssertIsOnMainThread(#function) guard let callData = self.callData else { // This should never happen; return to a known good state. owsFail("\(self.logTag) received data message, but there is no current call. Ignoring.") OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) received data message, but there is no current call. Ignoring.")) return } let call = callData.call if message.hasConnected { Logger.debug("\(self.logTag) remote participant sent Connected via data channel: \(call.identifiersForLogs).") let connected = message.connected! guard connected.id == call.signalingId else { // This should never happen; return to a known good state. owsFail("\(self.logTag) received connected message for call with id:\(connected.id) but current call has id:\(call.signalingId)") OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) received connected message for call with id:\(connected.id) but current call has id:\(call.signalingId)")) return } self.callUIAdapter.recipientAcceptedCall(call) handleConnectedCall(callData) } else if message.hasHangup { Logger.debug("\(self.logTag) remote participant sent Hangup via data channel: \(call.identifiersForLogs).") let hangup = message.hangup! guard hangup.id == call.signalingId else { // This should never happen; return to a known good state. owsFail("\(self.logTag) received hangup message for call with id:\(hangup.id) but current call has id:\(call.signalingId)") OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "\(self.logTag) received hangup message for call with id:\(hangup.id) but current call has id:\(call.signalingId)")) return } handleRemoteHangup(thread: call.thread, callId: hangup.id) } else if message.hasVideoStreamingStatus { Logger.debug("\(self.logTag) remote participant sent VideoStreamingStatus via data channel: \(call.identifiersForLogs).") guard let videoStreamingStatus = message.videoStreamingStatus else { owsFail("\(logTag) missing videoStreamingStatus") return } callData.isRemoteVideoEnabled = videoStreamingStatus.enabled self.fireDidUpdateVideoTracks() } else { Logger.info("\(self.logTag) received unknown or empty DataChannelMessage: \(call.identifiersForLogs).") } } // MARK: - PeerConnectionClientDelegate /** * The connection has been established. The clients can now communicate. */ internal func peerConnectionClientIceConnected(_ peerConnectionClient: PeerConnectionClient) { SwiftAssertIsOnMainThread(#function) guard peerConnectionClient == self.peerConnectionClient else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } self.handleIceConnected() } func peerConnectionClientIceDisconnected(_ peerconnectionClient: PeerConnectionClient) { SwiftAssertIsOnMainThread(#function) guard peerConnectionClient == self.peerConnectionClient else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } self.handleIceDisconnected() } /** * The connection failed to establish. The clients will not be able to communicate. */ internal func peerConnectionClientIceFailed(_ peerConnectionClient: PeerConnectionClient) { SwiftAssertIsOnMainThread(#function) guard peerConnectionClient == self.peerConnectionClient else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } // Return to a known good state. self.handleFailedCurrentCall(error: CallError.disconnected) } /** * During the Signaling process each client generates IceCandidates locally, which contain information about how to * reach the local client via the internet. The delegate must shuttle these IceCandates to the other (remote) client * out of band, as part of establishing a connection over WebRTC. */ internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate) { SwiftAssertIsOnMainThread(#function) guard peerConnectionClient == self.peerConnectionClient else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } self.handleLocalAddedIceCandidate(iceCandidate) } /** * Once the peerconnection is established, we can receive messages via the data channel, and notify the delegate. */ internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, received dataChannelMessage: WebRTCProtoData) { SwiftAssertIsOnMainThread(#function) guard peerConnectionClient == self.peerConnectionClient else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } self.handleDataChannelMessage(dataChannelMessage) } internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, didUpdateLocalVideoCaptureSession captureSession: AVCaptureSession?) { SwiftAssertIsOnMainThread(#function) guard peerConnectionClient == self.peerConnectionClient else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } guard let callData = callData else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } callData.localCaptureSession = captureSession fireDidUpdateVideoTracks() } internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, didUpdateRemoteVideoTrack videoTrack: RTCVideoTrack?) { SwiftAssertIsOnMainThread(#function) guard peerConnectionClient == self.peerConnectionClient else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } guard let callData = callData else { Logger.debug("\(self.logTag) \(#function) Ignoring event from obsolete peerConnectionClient") return } callData.remoteVideoTrack = videoTrack fireDidUpdateVideoTracks() } // MARK: - /** * RTCIceServers are used when attempting to establish an optimal connection to the other party. SignalService supplies * a list of servers, plus we have fallback servers hardcoded in the app. */ private func getIceServers() -> Promise<[RTCIceServer]> { SwiftAssertIsOnMainThread(#function) return firstly { return accountManager.getTurnServerInfo() }.then { turnServerInfo -> [RTCIceServer] in Logger.debug("\(self.logTag) got turn server urls: \(turnServerInfo.urls)") return turnServerInfo.urls.map { url in if url.hasPrefix("turn") { // 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]) } } + [CallService.fallbackIceServer] }.recover { error -> [RTCIceServer] in Logger.error("\(self.logTag) fetching ICE servers failed with error: \(error)") Logger.warn("\(self.logTag) using fallback ICE Servers") return [CallService.fallbackIceServer] } } // This method should be called when either: a) we know or assume that // the error is related to the current call. b) the error is so serious // that we want to terminate the current call (if any) in order to // return to a known good state. public func handleFailedCurrentCall(error: CallError) { Logger.debug("\(self.logTag) in \(#function)") // Return to a known good state by ending the current call, if any. handleFailedCall(failedCall: self.call, error: error) } // This method should be called when a fatal error occurred for a call. // // * If we know which call it was, we should update that call's state // to reflect the error. // * IFF that call is the current call, we want to terminate it. public func handleFailedCall(failedCall: SignalCall?, error: CallError) { SwiftAssertIsOnMainThread(#function) if case CallError.assertionError(description: let description) = error { owsFail(description) } if let failedCall = failedCall { switch failedCall.state { case .answering, .localRinging: assert(failedCall.callRecord == nil) // call failed before any call record could be created, make one now. handleMissedCall(failedCall) default: assert(failedCall.callRecord != nil) } // It's essential to set call.state before terminateCall, because terminateCall nils self.call failedCall.error = error failedCall.state = .localFailure self.callUIAdapter.failCall(failedCall, error: error) // Only terminate the current call if the error pertains to the current call. guard failedCall == self.call else { Logger.debug("\(self.logTag) in \(#function) ignoring obsolete call: \(failedCall.identifiersForLogs).") return } Logger.error("\(self.logTag) call: \(failedCall.identifiersForLogs) failed with error: \(error)") } else { Logger.error("\(self.logTag) unknown call failed with error: \(error)") } // Only terminate the call if it is the current call. terminateCall() } /** * Clean up any existing call state and get ready to receive a new call. */ private func terminateCall() { SwiftAssertIsOnMainThread(#function) Logger.debug("\(self.logTag) in \(#function)") let currentCallData = self.callData self.callData = nil currentCallData?.terminate() self.callUIAdapter.didTerminateCall(currentCallData?.call) fireDidUpdateVideoTracks() } // MARK: - CallObserver internal func stateDidChange(call: SignalCall, state: CallState) { SwiftAssertIsOnMainThread(#function) Logger.info("\(self.logTag) \(#function): \(state)") updateIsVideoEnabled() } internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) { SwiftAssertIsOnMainThread(#function) Logger.info("\(self.logTag) \(#function): \(hasLocalVideo)") self.updateIsVideoEnabled() } internal func muteDidChange(call: SignalCall, isMuted: Bool) { SwiftAssertIsOnMainThread(#function) // Do nothing } internal func holdDidChange(call: SignalCall, isOnHold: Bool) { SwiftAssertIsOnMainThread(#function) // Do nothing } internal func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?) { SwiftAssertIsOnMainThread(#function) // Do nothing } // MARK: - Video private func shouldHaveLocalVideoTrack() -> Bool { SwiftAssertIsOnMainThread(#function) guard let call = self.call else { return false } // The iOS simulator doesn't provide any sort of camera capture // support or emulation (http://goo.gl/rHAnC1) so don't bother // trying to open a local stream. return (!Platform.isSimulator && UIApplication.shared.applicationState != .background && call.state == .connected && call.hasLocalVideo) } //TODO only fire this when it's changed? as of right now it gets called whenever you e.g. lock the phone while it's incoming ringing. private func updateIsVideoEnabled() { SwiftAssertIsOnMainThread(#function) guard let call = self.call else { return } guard let peerConnectionClient = self.peerConnectionClient else { return } let shouldHaveLocalVideoTrack = self.shouldHaveLocalVideoTrack() Logger.info("\(self.logTag) \(#function): \(shouldHaveLocalVideoTrack)") self.peerConnectionClient?.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) var messageData: Data do { let videoStreamingStatusBuilder = WebRTCProtoVideoStreamingStatus.WebRTCProtoVideoStreamingStatusBuilder(id: call.signalingId) videoStreamingStatusBuilder.setEnabled(shouldHaveLocalVideoTrack) let dataBuilder = WebRTCProtoData.WebRTCProtoDataBuilder() dataBuilder.setVideoStreamingStatus(try videoStreamingStatusBuilder.build()) messageData = try dataBuilder.buildSerializedData() } catch { Logger.error("\(self.logTag) couldn't build proto in \(#function)") return } peerConnectionClient.sendDataChannelMessage(data: messageData, description: "videoStreamingStatus", isCritical: false) } // MARK: - Observers // The observer-related methods should be invoked on the main thread. func addObserverAndSyncState(observer: CallServiceObserver) { SwiftAssertIsOnMainThread(#function) observers.append(Weak(value: observer)) // Synchronize observer with current call state let remoteVideoTrack = self.isRemoteVideoEnabled ? self.remoteVideoTrack : nil observer.didUpdateVideoTracks(call: self.call, localCaptureSession: self.localCaptureSession, remoteVideoTrack: remoteVideoTrack) } // The observer-related methods should be invoked on the main thread. func removeObserver(_ observer: CallServiceObserver) { SwiftAssertIsOnMainThread(#function) while let index = observers.index(where: { $0.value === observer }) { observers.remove(at: index) } } // The observer-related methods should be invoked on the main thread. func removeAllObservers() { SwiftAssertIsOnMainThread(#function) observers = [] } private func fireDidUpdateVideoTracks() { SwiftAssertIsOnMainThread(#function) let remoteVideoTrack = self.isRemoteVideoEnabled ? self.remoteVideoTrack : nil for observer in observers { observer.value?.didUpdateVideoTracks(call: self.call, localCaptureSession: self.localCaptureSession, remoteVideoTrack: remoteVideoTrack) } } // MARK: CallViewController Timer var activeCallTimer: Timer? func startCallTimer() { SwiftAssertIsOnMainThread(#function) stopAnyCallTimer() assert(self.activeCallTimer == nil) self.activeCallTimer = WeakTimer.scheduledTimer(timeInterval: 1, target: self, userInfo: nil, repeats: true) { [weak self] timer in guard let strongSelf = self else { return } guard let call = strongSelf.call else { owsFail("\(strongSelf.logTag) call has since ended. Timer should have been invalidated.") timer.invalidate() return } strongSelf.ensureCallScreenPresented(call: call) } } func ensureCallScreenPresented(call: SignalCall) { guard let currentCall = self.call else { owsFail("\(self.logTag) obsolete call: \(call.identifiersForLogs) in \(#function)") return } guard currentCall == call else { owsFail("\(self.logTag) obsolete call: \(call.identifiersForLogs) in \(#function)") return } guard let connectedDate = call.connectedDate else { // Ignore; call hasn't connected yet. return } let kMaxViewPresentationDelay: Double = 5 guard fabs(connectedDate.timeIntervalSinceNow) > kMaxViewPresentationDelay else { // Ignore; call connected recently. return } guard !call.isTerminated else { // There's a brief window between when the callViewController is removed // and when this timer is terminated. // // We don't want to fail a call that's already terminated. Logger.debug("\(logTag) in \(#function) ignoring screen protection check for already terminated call.") return } if !OWSWindowManager.shared().hasCall() { OWSProdError(OWSAnalyticsEvents.callServiceCallViewCouldNotPresent(), file: #file, function: #function, line: #line) owsFail("\(self.logTag) in \(#function) Call terminated due to missing call view.") self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "Call view didn't present after \(kMaxViewPresentationDelay) seconds")) return } } func stopAnyCallTimer() { SwiftAssertIsOnMainThread(#function) self.activeCallTimer?.invalidate() self.activeCallTimer = nil } }