//// //// Copyright (c) 2019 Open Whisper Systems. All rights reserved. //// // //import Foundation //import PromiseKit //import WebRTC //import SignalUtilitiesKit //import SignalUtilitiesKit // ///** // * `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) // case messageSendFailure(underlyingError: Error) //} // //// 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?) //} // //protocol SignalCallDataDelegate: class { // func outgoingIceUpdateDidFail(call: SignalCall, error: Error) //} // //// Gather all per-call state in one place. //private class SignalCallData: NSObject { // // fileprivate weak var delegate: SignalCallDataDelegate? // // public let call: SignalCall // // // Used to coordinate promises across delegate methods // let callConnectedPromise: Promise // let callConnectedResolver: Resolver // // // Used to ensure any received ICE messages wait until the peer connection client is set up. // let peerConnectionClientPromise: Promise // let peerConnectionClientResolver: Resolver // // // Used to ensure CallOffer was sent before sending any ICE updates. // let readyToSendIceUpdatesPromise: Promise // let readyToSendIceUpdatesResolver: Resolver // // weak var localCaptureSession: AVCaptureSession? { // didSet { // AssertIsOnMainThread() // // Logger.info("") // } // } // // weak var remoteVideoTrack: RTCVideoTrack? { // didSet { // AssertIsOnMainThread() // // Logger.info("") // } // } // // var isRemoteVideoEnabled = false { // didSet { // AssertIsOnMainThread() // // Logger.info("\(isRemoteVideoEnabled)") // } // } // // var peerConnectionClient: PeerConnectionClient? { // didSet { // AssertIsOnMainThread() // // Logger.debug(".peerConnectionClient setter: \(oldValue != nil) -> \(peerConnectionClient != nil) \(String(describing: peerConnectionClient))") // } // } // // required init(call: SignalCall, delegate: SignalCallDataDelegate) { // self.call = call // self.delegate = delegate // // let (callConnectedPromise, callConnectedResolver) = Promise.pending() // self.callConnectedPromise = callConnectedPromise // self.callConnectedResolver = callConnectedResolver // // let (peerConnectionClientPromise, peerConnectionClientResolver) = Promise.pending() // self.peerConnectionClientPromise = peerConnectionClientPromise // self.peerConnectionClientResolver = peerConnectionClientResolver // // let (readyToSendIceUpdatesPromise, readyToSendIceUpdatesResolver) = Promise.pending() // self.readyToSendIceUpdatesPromise = readyToSendIceUpdatesPromise // self.readyToSendIceUpdatesResolver = readyToSendIceUpdatesResolver // // super.init() // } // // deinit { // Logger.debug("[SignalCallData] deinit") // } // // // MARK: - // // public func terminate() { // AssertIsOnMainThread() // // Logger.debug("") // // 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. // self.callConnectedResolver.reject(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. // self.peerConnectionClientResolver.reject(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. // self.readyToSendIceUpdatesResolver.reject(CallError.obsoleteCall(description: "Terminating call")) // // peerConnectionClient?.terminate() // Logger.debug("setting peerConnectionClient") // // outgoingIceUpdateQueue.removeAll() // } // // // MARK: - Dependencies // // private var messageSender: MessageSender { // return SSKEnvironment.shared.messageSender // } // // // MARK: - Outgoing ICE updates. // // // Setting up a call involves sending many (currently 20+) ICE updates. // // We send messages serially in order to preserve outgoing message order. // // There are so many ICE updates per call that the cost of sending all of // // those messages becomes significant. So we batch outgoing ICE updates, // // making sure that we only have one outgoing ICE update message at a time. // // // // This variable should only be accessed on the main thread. // private var outgoingIceUpdateQueue = [SSKProtoCallMessageIceUpdate]() // private var outgoingIceUpdatesInFlight = false // // func sendOrEnqueue(outgoingIceUpdate iceUpdateProto: SSKProtoCallMessageIceUpdate) { // AssertIsOnMainThread() // // outgoingIceUpdateQueue.append(iceUpdateProto) // // tryToSendIceUpdates() // } // // private func tryToSendIceUpdates() { // AssertIsOnMainThread() // // guard !outgoingIceUpdatesInFlight else { // Logger.verbose("Enqueued outgoing ice update") // return // } // // let iceUpdateProtos = outgoingIceUpdateQueue // guard iceUpdateProtos.count > 0 else { // // Nothing in the queue. // return // } // // outgoingIceUpdateQueue.removeAll() // outgoingIceUpdatesInFlight = true // // /** // * 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 callMessage = OWSOutgoingCallMessage(thread: call.thread, iceUpdateMessages: iceUpdateProtos) // let sendPromise = self.messageSender.sendPromise(message: callMessage) // .done { [weak self] in // AssertIsOnMainThread() // // guard let strongSelf = self else { // return // } // // strongSelf.outgoingIceUpdatesInFlight = false // strongSelf.tryToSendIceUpdates() // }.catch { [weak self] (error) in // AssertIsOnMainThread() // // guard let strongSelf = self else { // return // } // // strongSelf.outgoingIceUpdatesInFlight = false // strongSelf.delegate?.outgoingIceUpdateDidFail(call: strongSelf.call, error: error) // } // sendPromise.retainUntilComplete() // } //} // //// This class' state should only be accessed on the main queue. //@objc public class CallService: NSObject, CallObserver, PeerConnectionClientDelegate, SignalCallDataDelegate { // // // MARK: - Properties // // var observers = [Weak]() // // // Exposed by environment.m // // @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 { // AssertIsOnMainThread() // // oldValue?.delegate = nil // 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(".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 { // AssertIsOnMainThread() // // return callData?.call // } // } // var peerConnectionClient: PeerConnectionClient? { // get { // AssertIsOnMainThread() // // return callData?.peerConnectionClient // } // } // // weak var localCaptureSession: AVCaptureSession? { // get { // AssertIsOnMainThread() // // return callData?.localCaptureSession // } // } // // var remoteVideoTrack: RTCVideoTrack? { // get { // AssertIsOnMainThread() // // return callData?.remoteVideoTrack // } // } // var isRemoteVideoEnabled: Bool { // get { // AssertIsOnMainThread() // // guard let callData = callData else { // return false // } // return callData.isRemoteVideoEnabled // } // } // // @objc public override init() { // // super.init() // // SwiftSingletons.register(self) // // 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) // } // // // MARK: - Dependencies // // private var contactsManager: OWSContactsManager { // return Environment.shared.contactsManager // } // // private var messageSender: MessageSender { // return SSKEnvironment.shared.messageSender // } // // private var accountManager: AccountManager { // return AppEnvironment.shared.accountManager // } // // private var notificationPresenter: NotificationPresenter { // return AppEnvironment.shared.notificationPresenter // } // // // MARK: - Notifications // // @objc func didEnterBackground() { // AssertIsOnMainThread() // self.updateIsVideoEnabled() // } // // @objc func didBecomeActive() { // AssertIsOnMainThread() // self.updateIsVideoEnabled() // } // // /** // * Choose whether to use CallKit or a Notification backed interface for calling. // */ // @objc public func createCallUIAdapter() { // AssertIsOnMainThread() // // if self.call != nil { // Logger.warn("ending current call in. Did user toggle callkit preference while in a call?") // self.terminateCall() // } // self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationPresenter: self.notificationPresenter) // } // // // MARK: - Service Actions // // /** // * Initiate an outgoing call. // */ // func handleOutgoingCall(_ call: SignalCall) -> Promise { // AssertIsOnMainThread() // // let callId = call.signalingId // BenchEventStart(title: "Outgoing Call Connection", eventId: "call-\(callId)") // // guard self.call == nil else { // let errorDescription = "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, delegate: self) // self.callData = callData // // // MJK TODO remove this timestamp param // 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("got ice servers:\(iceServers) for call: \(call.identifiersForLogs)") // // guard self.call == call else { // throw CallError.obsoleteCall(description: "obsolete call") // } // // guard callData.peerConnectionClient == nil else { // let errorDescription = "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.shared.preferences.doCallsHideIPAddress() // // let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .outgoing, useTurnOnly: useTurnOnly) // Logger.debug("setting peerConnectionClient for call: \(call.identifiersForLogs)") // callData.peerConnectionClient = peerConnectionClient // callData.peerConnectionClientResolver.fulfill(()) // // return peerConnectionClient.createOffer() // }.then { (sessionDescription: HardenedRTCSessionDescription) -> Promise in // guard self.call == call else { // throw CallError.obsoleteCall(description: "obsolete call") // } // guard let peerConnectionClient = self.peerConnectionClient else { // owsFailDebug("Missing peerConnectionClient") // throw CallError.obsoleteCall(description: "Missing peerConnectionClient") // } // // Logger.info("session description for outgoing call: \(call.identifiersForLogs), sdp: \(sessionDescription.logSafeDescription).") // // return // peerConnectionClient.setLocalSessionDescription(sessionDescription) // .then { _ -> Promise in // do { // let offerBuilder = SSKProtoCallMessageOffer.builder(id: call.signalingId, // sessionDescription: sessionDescription.sdp) // let callMessage = OWSOutgoingCallMessage(thread: call.thread, offerMessage: try offerBuilder.build()) // return self.messageSender.sendPromise(message: callMessage) // } catch { // owsFailDebug("Couldn't build proto") // throw CallError.fatalError(description: "Couldn't build proto") // } // } // }.then { () -> Promise in // guard self.call == call else { // throw CallError.obsoleteCall(description: "obsolete call") // } // // // 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(seconds: connectingTimeoutSeconds).done { // // 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) // }.done { // Logger.info(self.call == call // ? "outgoing call connected: \(call.identifiersForLogs)." // : "obsolete outgoing call connected: \(call.identifiersForLogs).") // } // // promise.catch { error in // Logger.error("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) // } // }.retainUntilComplete() // // return promise // } // // func readyToSendIceUpdates(call: SignalCall) { // AssertIsOnMainThread() // // guard let callData = self.callData else { // self.handleFailedCall(failedCall: call, error: .obsoleteCall(description:"obsolete call")) // return // } // guard callData.call == call else { // Logger.warn("ignoring \(#function) for call other than current call") // return // } // // callData.readyToSendIceUpdatesResolver.fulfill(()) // } // // /** // * Called by the call initiator after receiving a CallAnswer from the callee. // */ // public func handleReceivedAnswer(thread: TSContactThread, callId: UInt64, sessionDescription: String) { // AssertIsOnMainThread() // Logger.info("received call answer for call: \(callId) thread: \(thread.contactIdentifier())") // // guard let call = self.call else { // Logger.warn("ignoring obsolete call: \(callId)") // return // } // // guard call.signalingId == callId else { // Logger.warn("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") // 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")) // return // } // // let sessionDescription = RTCSessionDescription(type: .answer, sdp: sessionDescription) // // peerConnectionClient.setRemoteSessionDescription(sessionDescription) // .done { // Logger.debug("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) // } // }.retainUntilComplete() // } // // /** // * User didn't answer incoming call // */ // public func handleMissedCall(_ call: SignalCall) { // AssertIsOnMainThread() // // if call.callRecord == nil { // // MJK TODO remove this timestamp param // call.callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), // withCallNumber: call.thread.contactIdentifier(), // callType: RPRecentCallTypeIncomingMissed, // in: call.thread) // } // // guard let callRecord = call.callRecord else { // handleFailedCall(failedCall: call, error: .assertionError(description: "callRecord was unexpectedly nil")) // return // } // // switch callRecord.callType { // case RPRecentCallTypeIncomingMissed: // callRecord.save() // callUIAdapter.reportMissedCall(call) // case RPRecentCallTypeIncomingIncomplete, RPRecentCallTypeIncoming: // callRecord.updateCallType(RPRecentCallTypeIncomingMissed) // callUIAdapter.reportMissedCall(call) // case RPRecentCallTypeOutgoingIncomplete: // callRecord.updateCallType(RPRecentCallTypeOutgoingMissed) // case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity, RPRecentCallTypeIncomingDeclined, RPRecentCallTypeOutgoingMissed, RPRecentCallTypeOutgoing: // owsFailDebug("unexpected RPRecentCallType: \(callRecord.callType)") // callRecord.save() // default: // callRecord.save() // owsFailDebug("unknown RPRecentCallType: \(callRecord.callType)") // } // } // // /** // * Received a call while already in another call. // */ // private func handleLocalBusyCall(_ call: SignalCall) { // AssertIsOnMainThread() // Logger.info("for call: \(call.identifiersForLogs) thread: \(call.thread.contactIdentifier())") // // do { // let busyBuilder = SSKProtoCallMessageBusy.builder(id: call.signalingId) // let callMessage = OWSOutgoingCallMessage(thread: call.thread, busyMessage: try busyBuilder.build()) // let sendPromise = messageSender.sendPromise(message: callMessage) // sendPromise.retainUntilComplete() // // handleMissedCall(call) // } catch { // owsFailDebug("Couldn't build proto") // } // } // // /** // * The callee was already in another call. // */ // public func handleRemoteBusy(thread: TSContactThread, callId: UInt64) { // AssertIsOnMainThread() // Logger.info("for thread: \(thread.contactIdentifier())") // // guard let call = self.call else { // Logger.warn("ignoring obsolete call: \(callId)") // return // } // // guard call.signalingId == callId else { // Logger.warn("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") // return // } // // guard thread.contactIdentifier() == call.remotePhoneNumber else { // Logger.warn("ignoring obsolete call") // return // } // // call.state = .remoteBusy // assert(call.callRecord != nil) // call.callRecord?.updateCallType(RPRecentCallTypeOutgoingMissed) // // 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) { // AssertIsOnMainThread() // // BenchEventStart(title: "Incoming Call Connection", eventId: "call-\(callId)") // // let newCall = SignalCall.incomingCall(localId: UUID(), remotePhoneNumber: thread.contactIdentifier(), signalingId: callId) // // Logger.info("receivedCallOffer: \(newCall.identifiersForLogs)") // // let untrustedIdentity = OWSIdentityManager.shared().untrustedIdentityForSending(toRecipientId: thread.contactIdentifier()) // // guard untrustedIdentity == nil else { // Logger.warn("missed a call due to untrusted identity: \(newCall.identifiersForLogs)") // // let callerName = self.contactsManager.displayName(forPhoneIdentifier: thread.contactIdentifier()) // // switch untrustedIdentity!.verificationState { // case .verified: // owsFailDebug("shouldn't have missed a call due to untrusted identity if the identity is verified") // self.notificationPresenter.presentMissedCall(newCall, callerName: callerName) // case .default: // self.notificationPresenter.presentMissedCallBecauseOfNewIdentity(call: newCall, callerName: callerName) // case .noLongerVerified: // self.notificationPresenter.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: newCall, callerName: callerName) // } // // // MJK TODO remove this timestamp param // 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("receivedCallOffer: \(newCall.identifiersForLogs) but we're already in call: \(existingCall.identifiersForLogs)") // // handleLocalBusyCall(newCall) // // if existingCall.remotePhoneNumber == newCall.remotePhoneNumber { // Logger.info("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("starting new call: \(newCall.identifiersForLogs)") // // let callData = SignalCallData(call: newCall, delegate: self) // self.callData = callData // // var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in // AssertIsOnMainThread() // // 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("ignoring obsolete call") // return // } // strongSelf.handleFailedCall(failedCall: newCall, error: timeout) // }) // // 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.shared.preferences.doCallsHideIPAddress() // // Logger.debug("setting peerConnectionClient for: \(newCall.identifiersForLogs)") // let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .incoming, useTurnOnly: useTurnOnly) // callData.peerConnectionClient = peerConnectionClient // callData.peerConnectionClientResolver.fulfill(()) // // 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) -> Promise in // guard self.call == newCall else { // throw CallError.obsoleteCall(description: "negotiateSessionDescription() response for obsolete call") // } // // Logger.info("session description for incoming call: \(newCall.identifiersForLogs), sdp: \(negotiatedSessionDescription.logSafeDescription).") // // do { // let answerBuilder = SSKProtoCallMessageAnswer.builder(id: newCall.signalingId, // sessionDescription: negotiatedSessionDescription.sdp) // let callAnswerMessage = OWSOutgoingCallMessage(thread: thread, answerMessage: try answerBuilder.build()) // // return self.messageSender.sendPromise(message: callAnswerMessage) // } catch { // owsFailDebug("Couldn't build proto") // throw CallError.fatalError(description: "Couldn't build proto") // } // }.then { () -> Promise in // guard self.call == newCall else { // throw CallError.obsoleteCall(description: "sendPromise(message: ) response for obsolete call") // } // Logger.debug("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(seconds: connectingTimeoutSeconds).done { // // 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) // }.done { // Logger.info(self.call == newCall // ? "incoming call connected: \(newCall.identifiersForLogs)." // : "obsolete incoming call connected: \(newCall.identifiersForLogs).") // }.recover { error in // guard self.call == newCall else { // Logger.debug("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) // } // }.ensure { // Logger.debug("ending background task awaiting inbound call connection") // // assert(backgroundTask != nil) // backgroundTask = nil // }.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) { // AssertIsOnMainThread() // Logger.verbose("callId: \(callId)") // // guard let callData = self.callData else { // Logger.info("ignoring remote ice update, since there is no current call.") // return // } // // callData.peerConnectionClientPromise.done { // AssertIsOnMainThread() // // 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("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") // 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("addRemoteIceCandidate") // peerConnectionClient.addRemoteIceCandidate(RTCIceCandidate(sdp: sdp, sdpMLineIndex: lineIndex, sdpMid: mid)) // }.catch { error in // OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleRemoteAddedIceCandidate(), file: #file, function: #function, line: #line) // Logger.error("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) { // AssertIsOnMainThread() // // 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.done { // 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 { // owsFailDebug("Missing sdpMid") // throw CallError.fatalError(description: "Missing sdpMid") // } // // guard iceCandidate.sdpMLineIndex < UINT32_MAX else { // owsFailDebug("Invalid sdpMLineIndex") // throw CallError.fatalError(description: "Invalid sdpMLineIndex") // } // // Logger.info("sending ICE Candidate \(call.identifiersForLogs).") // // let iceUpdateProto: SSKProtoCallMessageIceUpdate // do { // let iceUpdateBuilder = SSKProtoCallMessageIceUpdate.builder(id: call.signalingId, // sdpMid: sdpMid, // sdpMlineIndex: UInt32(iceCandidate.sdpMLineIndex), // sdp: iceCandidate.sdp) // iceUpdateProto = try iceUpdateBuilder.build() // } catch { // owsFailDebug("Couldn't build proto") // throw CallError.fatalError(description: "Couldn't build proto") // } // // /** // * 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. // */ // callData.sendOrEnqueue(outgoingIceUpdate: iceUpdateProto) // }.catch { error in // OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalAddedIceCandidate(), file: #file, function: #function, line: #line) // Logger.error("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() { // AssertIsOnMainThread() // // guard let callData = self.callData 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: "ignoring \(#function) since there is no current call.")) // return // } // let call = callData.call // let callId = call.signalingId // // Logger.info("\(call.identifiersForLogs)") // // switch call.state { // case .dialing: // if call.state != .remoteRinging { // BenchEventComplete(eventId: "call-\(callId)") // } // call.state = .remoteRinging // case .answering: // if call.state != .localRinging { // BenchEventComplete(eventId: "call-\(callId)") // } // call.state = .localRinging // self.callUIAdapter.reportIncomingCall(call, thread: call.thread) // case .remoteRinging: // Logger.info("call already ringing. Ignoring \(#function): \(call.identifiersForLogs).") // case .connected: // Logger.info("Call reconnected \(#function): \(call.identifiersForLogs).") // case .reconnecting: // call.state = .connected // case .idle, .localRinging, .localFailure, .localHangup, .remoteHangup, .remoteBusy: // owsFailDebug("unexpected call state: \(call.state): \(call.identifiersForLogs).") // } // } // // private func handleIceDisconnected() { // AssertIsOnMainThread() // // 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: "ignoring \(#function) since there is no current call.")) // return // } // // Logger.info("\(call.identifiersForLogs).") // // switch call.state { // case .remoteRinging, .localRinging: // Logger.debug("disconnect while ringing... we'll keep ringing") // case .connected: // call.state = .reconnecting // default: // owsFailDebug("unexpected call state: \(call.state): \(call.identifiersForLogs).") // } // } // // /** // * The remote client (caller or callee) ended the call. // */ // public func handleRemoteHangup(thread: TSContactThread, callId: UInt64) { // AssertIsOnMainThread() // Logger.debug("") // // guard let call = self.call else { // // This may happen if we hang up slightly before they hang up. // handleFailedCurrentCall(error: .obsoleteCall(description:"call was unexpectedly nil")) // return // } // // guard call.signalingId == callId else { // Logger.warn("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") // 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("ignoring hangup for thread: \(thread.contactIdentifier()) which is not the current call: \(call.identifiersForLogs)") // return // } // // Logger.info("\(call.identifiersForLogs).") // // switch call.state { // case .idle, .dialing, .answering, .localRinging, .localFailure, .remoteBusy, .remoteRinging: // handleMissedCall(call) // case .connected, .reconnecting, .localHangup, .remoteHangup: // Logger.info("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) { // AssertIsOnMainThread() // // guard let call = self.call else { // // This should never happen; return to a known good state. // owsFailDebug("call was unexpectedly nil") // OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) // handleFailedCurrentCall(error: CallError.assertionError(description: "call was unexpectedly nil")) // return // } // // guard call.localId == localId else { // // This should never happen; return to a known good state. // owsFailDebug("callLocalId:\(localId) doesn't match current calls: \(call.localId)") // OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) // handleFailedCurrentCall(error: CallError.assertionError(description: "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) { // AssertIsOnMainThread() // // Logger.debug("") // // guard let currentCallData = self.callData else { // OWSProdError(OWSAnalyticsEvents.callServiceCallDataMissing(), file: #file, function: #function, line: #line) // handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callData unexpectedly nil")) // 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("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: "missing peerconnection client")) // return // } // // Logger.info("\(call.identifiersForLogs).") // // // MJK TODO remove this timestamp param // 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.builder(id: call.signalingId) // let dataBuilder = WebRTCProtoData.builder() // dataBuilder.setConnected(try connectedBuilder.build()) // messageData = try dataBuilder.buildSerializedData() // } catch { // handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) // 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) { // AssertIsOnMainThread() // Logger.info("") // // guard let peerConnectionClient = callData.peerConnectionClient else { // OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) // handleFailedCall(failedCall: call, error: CallError.assertionError(description: "peerConnectionClient unexpectedly nil")) // return // } // // Logger.info("handleConnectedCall: \(callData.call.identifiersForLogs).") // // // cancel connection timeout // callData.callConnectedResolver.fulfill(()) // // 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) { // AssertIsOnMainThread() // // guard let call = self.call else { // // This should never happen; return to a known good state. // owsFailDebug("call was unexpectedly nil") // OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) // handleFailedCurrentCall(error: CallError.assertionError(description: "call was unexpectedly nil")) // return // } // // guard call.localId == localId else { // // This should never happen; return to a known good state. // owsFailDebug("callLocalId:\(localId) doesn't match current calls: \(call.localId)") // OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) // handleFailedCurrentCall(error: CallError.assertionError(description: "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) { // AssertIsOnMainThread() // // Logger.info("\(call.identifiersForLogs).") // // if let callRecord = call.callRecord { // owsFailDebug("Not expecting callrecord to already be set") // callRecord.updateCallType(RPRecentCallTypeIncomingDeclined) // } else { // // MJK TODO remove this timestamp param // 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) { // AssertIsOnMainThread() // // guard let currentCall = self.call else { // Logger.info("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: "ignoring \(#function) for call other than current call")) // return // } // // Logger.info("\(call.identifiersForLogs).") // // call.state = .localHangup // // if let callRecord = call.callRecord { // if callRecord.callType == RPRecentCallTypeOutgoingIncomplete { // callRecord.updateCallType(RPRecentCallTypeOutgoingMissed) // } // } else { // owsFailDebug("missing call record") // } // // // 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.builder(id: call.signalingId) // let dataBuilder = WebRTCProtoData.builder() // dataBuilder.setHangup(try hangupBuilder.build()) // messageData = try dataBuilder.buildSerializedData() // } catch { // handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) // return // } // // peerConnectionClient.sendDataChannelMessage(data: messageData, description: "hangup", isCritical: true) // } else { // Logger.info("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.builder(id: call.signalingId) // let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) // // self.messageSender.sendPromise(message: callMessage) // .done { // Logger.debug("successfully sent hangup call message to \(call.thread.contactIdentifier())") // }.catch { error in // OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalHungupCall(), file: #file, function: #function, line: #line) // Logger.error("failed to send hangup call message to \(call.thread.contactIdentifier()) with error: \(error)") // }.retainUntilComplete() // // terminateCall() // } catch { // handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) // } // } // // /** // * Local user toggled to mute audio. // * // * Can be used for Incoming and Outgoing calls. // */ // func setIsMuted(call: SignalCall, isMuted: Bool) { // AssertIsOnMainThread() // // 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("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) { // AssertIsOnMainThread() // // guard call == self.call else { // Logger.info("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) { // AssertIsOnMainThread() // // guard let frontmostViewController = UIApplication.shared.frontmostViewController else { // owsFailDebug("could not identify frontmostViewController") // 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) { // AssertIsOnMainThread() // // guard let call = self.call else { // // This can happen if you toggle local video right after // // the other user ends the call. // Logger.debug("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() { // AssertIsOnMainThread() // // self.setHasLocalVideo(hasLocalVideo: true) // } // // func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) { // AssertIsOnMainThread() // // 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) { // AssertIsOnMainThread() // // guard let callData = self.callData else { // // This should never happen; return to a known good state. // owsFailDebug("received data message, but there is no current call. Ignoring.") // OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) // handleFailedCurrentCall(error: CallError.assertionError(description: "received data message, but there is no current call. Ignoring.")) // return // } // let call = callData.call // // if let connected = message.connected { // Logger.debug("remote participant sent Connected via data channel: \(call.identifiersForLogs).") // // guard connected.id == call.signalingId else { // // This should never happen; return to a known good state. // owsFailDebug("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: "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 let hangup = message.hangup { // Logger.debug("remote participant sent Hangup via data channel: \(call.identifiersForLogs).") // // guard hangup.id == call.signalingId else { // // This should never happen; return to a known good state. // owsFailDebug("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: "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 let videoStreamingStatus = message.videoStreamingStatus { // Logger.debug("remote participant sent VideoStreamingStatus via data channel: \(call.identifiersForLogs).") // // callData.isRemoteVideoEnabled = videoStreamingStatus.enabled // self.fireDidUpdateVideoTracks() // } else { // Logger.info("received unknown or empty DataChannelMessage: \(call.identifiersForLogs).") // } // } // // // MARK: - PeerConnectionClientDelegate // // /** // * The connection has been established. The clients can now communicate. // */ // internal func peerConnectionClientIceConnected(_ peerConnectionClient: PeerConnectionClient) { // AssertIsOnMainThread() // // guard peerConnectionClient == self.peerConnectionClient else { // Logger.debug("Ignoring event from obsolete peerConnectionClient") // return // } // // self.handleIceConnected() // } // // func peerConnectionClientIceDisconnected(_ peerconnectionClient: PeerConnectionClient) { // AssertIsOnMainThread() // // guard peerConnectionClient == self.peerConnectionClient else { // Logger.debug("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) { // AssertIsOnMainThread() // // guard peerConnectionClient == self.peerConnectionClient else { // Logger.debug("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) { // AssertIsOnMainThread() // // guard peerConnectionClient == self.peerConnectionClient else { // Logger.debug("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) { // AssertIsOnMainThread() // // guard peerConnectionClient == self.peerConnectionClient else { // Logger.debug("Ignoring event from obsolete peerConnectionClient") // return // } // // self.handleDataChannelMessage(dataChannelMessage) // } // // internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, didUpdateLocalVideoCaptureSession captureSession: AVCaptureSession?) { // AssertIsOnMainThread() // // guard peerConnectionClient == self.peerConnectionClient else { // Logger.debug("Ignoring event from obsolete peerConnectionClient") // return // } // guard let callData = callData else { // Logger.debug("Ignoring event from obsolete peerConnectionClient") // return // } // // callData.localCaptureSession = captureSession // fireDidUpdateVideoTracks() // } // // internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, didUpdateRemoteVideoTrack videoTrack: RTCVideoTrack?) { // AssertIsOnMainThread() // // guard peerConnectionClient == self.peerConnectionClient else { // Logger.debug("Ignoring event from obsolete peerConnectionClient") // return // } // guard let callData = callData else { // Logger.debug("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]> { // // return self.accountManager.getTurnServerInfo() // .map(on: DispatchQueue.global()) { turnServerInfo -> [RTCIceServer] in // Logger.debug("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(on: DispatchQueue.global()) { (error: Error) -> Guarantee<[RTCIceServer]> in // Logger.error("fetching ICE servers failed with error: \(error)") // Logger.warn("using fallback ICE Servers") // // return Guarantee.value([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("") // // // 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) { // AssertIsOnMainThread() // // if case CallError.assertionError(description: let description) = error { // owsFailDebug(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("ignoring obsolete call: \(failedCall.identifiersForLogs).") // return // } // // Logger.error("call: \(failedCall.identifiersForLogs) failed with error: \(error)") // } else { // Logger.error("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() { // AssertIsOnMainThread() // // Logger.debug("") // // let currentCallData = self.callData // self.callData = nil // // currentCallData?.terminate() // // self.callUIAdapter.didTerminateCall(currentCallData?.call) // // fireDidUpdateVideoTracks() // // // Apparently WebRTC will sometimes disable device orientation notifications. // // After every call ends, we need to ensure they are enabled. // UIDevice.current.beginGeneratingDeviceOrientationNotifications() // } // // // MARK: - CallObserver // // internal func stateDidChange(call: SignalCall, state: CallState) { // AssertIsOnMainThread() // Logger.info("\(state)") // // updateIsVideoEnabled() // } // // internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) { // AssertIsOnMainThread() // Logger.info("\(hasLocalVideo)") // // self.updateIsVideoEnabled() // } // // internal func muteDidChange(call: SignalCall, isMuted: Bool) { // AssertIsOnMainThread() // // Do nothing // } // // internal func holdDidChange(call: SignalCall, isOnHold: Bool) { // AssertIsOnMainThread() // // Do nothing // } // // internal func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?) { // AssertIsOnMainThread() // // Do nothing // } // // // MARK: - Video // // private func shouldHaveLocalVideoTrack() -> Bool { // AssertIsOnMainThread() // // 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() { // AssertIsOnMainThread() // // guard let call = self.call else { // return // } // guard let peerConnectionClient = self.peerConnectionClient else { // return // } // // let shouldHaveLocalVideoTrack = self.shouldHaveLocalVideoTrack() // // Logger.info("\(shouldHaveLocalVideoTrack)") // // self.peerConnectionClient?.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) // // var messageData: Data // do { // let videoStreamingStatusBuilder = WebRTCProtoVideoStreamingStatus.builder(id: call.signalingId) // videoStreamingStatusBuilder.setEnabled(shouldHaveLocalVideoTrack) // let dataBuilder = WebRTCProtoData.builder() // dataBuilder.setVideoStreamingStatus(try videoStreamingStatusBuilder.build()) // messageData = try dataBuilder.buildSerializedData() // } catch { // Logger.error("couldn't build proto") // 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) { // AssertIsOnMainThread() // // 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) { // AssertIsOnMainThread() // // while let index = observers.firstIndex(where: { $0.value === observer }) { // observers.remove(at: index) // } // } // // // The observer-related methods should be invoked on the main thread. // func removeAllObservers() { // AssertIsOnMainThread() // // observers = [] // } // // private func fireDidUpdateVideoTracks() { // AssertIsOnMainThread() // // 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() { // AssertIsOnMainThread() // // 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 { // owsFailDebug("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 { // owsFailDebug("obsolete call: \(call.identifiersForLogs)") // return // } // guard currentCall == call else { // owsFailDebug("obsolete call: \(call.identifiersForLogs)") // 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("ignoring screen protection check for already terminated call.") // return // } // // if !OWSWindowManager.shared().hasCall() { // OWSProdError(OWSAnalyticsEvents.callServiceCallViewCouldNotPresent(), file: #file, function: #function, line: #line) // owsFailDebug("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() { // AssertIsOnMainThread() // // self.activeCallTimer?.invalidate() // self.activeCallTimer = nil // } // // // MARK: - SignalCallDataDelegate // // func outgoingIceUpdateDidFail(call: SignalCall, error: Error) { // AssertIsOnMainThread() // // guard self.call == call else { // Logger.warn("obsolete call") // return // } // // handleFailedCall(failedCall: call, error: CallError.messageSendFailure(underlyingError: error)) // } //} // //extension RPRecentCallType: CustomStringConvertible { // public var description: String { // switch self { // case RPRecentCallTypeIncoming: // return "RPRecentCallTypeIncoming" // case RPRecentCallTypeOutgoing: // return "RPRecentCallTypeOutgoing" // case RPRecentCallTypeIncomingMissed: // return "RPRecentCallTypeIncomingMissed" // case RPRecentCallTypeOutgoingIncomplete: // return "RPRecentCallTypeOutgoingIncomplete" // case RPRecentCallTypeIncomingIncomplete: // return "RPRecentCallTypeIncomingIncomplete" // case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity: // return "RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity" // case RPRecentCallTypeIncomingDeclined: // return "RPRecentCallTypeIncomingDeclined" // case RPRecentCallTypeOutgoingMissed: // return "RPRecentCallTypeOutgoingMissed" // default: // owsFailDebug("unexpected RPRecentCallType: \(self)") // return "RPRecentCallTypeUnknown" // } // } //}