diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 984ac793c..d2320a43b 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -2,12 +2,14 @@ import Foundation import WebRTC import SessionMessagingKit -public final class SessionCall: NSObject { +public final class SessionCall: NSObject, WebRTCSessionDelegate { // MARK: Metadata Properties let uuid: UUID let sessionID: String let mode: Mode let webRTCSession: WebRTCSession + var remoteSDP: RTCSessionDescription? = nil + var isWaitingForRemoteSDP = false var contactName: String { let contact = Storage.shared.getContact(with: self.sessionID) return contact?.displayName(for: Contact.Context.regular) ?? self.sessionID @@ -20,10 +22,40 @@ public final class SessionCall: NSObject { } } + // MARK: Control + lazy public var videoCapturer: RTCVideoCapturer = { + return RTCCameraVideoCapturer(delegate: webRTCSession.localVideoSource) + }() + + var isRemoteVideoEnabled = false { + didSet { + remoteVideoStateDidChange?(isRemoteVideoEnabled) + } + } + + var isMuted = false { + willSet { + if newValue { + webRTCSession.mute() + } else { + webRTCSession.unmute() + } + } + } + var isVideoEnabled = false { + willSet { + if newValue { + webRTCSession.turnOnVideo() + } else { + webRTCSession.turnOffVideo() + } + } + } + // MARK: Mode enum Mode { case offer - case answer(sdp: RTCSessionDescription) + case answer } // MARK: Call State Properties @@ -60,6 +92,7 @@ public final class SessionCall: NSObject { var hasStartedConnectingDidChange: (() -> Void)? var hasConnectedDidChange: (() -> Void)? var hasEndedDidChange: (() -> Void)? + var remoteVideoStateDidChange: ((Bool) -> Void)? // MARK: Derived Properties var hasStartedConnecting: Bool { @@ -92,15 +125,22 @@ public final class SessionCall: NSObject { self.mode = mode self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid) super.init() - reportIncomingCallIfNeeded() + self.webRTCSession.delegate = self } - func reportIncomingCallIfNeeded() { - guard case .answer(_) = mode else { return } + func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) { + guard case .answer = mode else { return } AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in - if let error = error { - SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") - } + completion(error) + } + } + + func didReceiveRemoteSDP(sdp: RTCSessionDescription) { + guard remoteSDP == nil else { return } + remoteSDP = sdp + if isWaitingForRemoteSDP { + webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally + isWaitingForRemoteSDP = false } } @@ -119,9 +159,13 @@ public final class SessionCall: NSObject { } func answerSessionCall(completion: (() -> Void)?) { - guard case let .answer(sdp) = mode else { return } + guard case .answer = mode else { return } hasStartedConnecting = true - webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally + if let sdp = remoteSDP { + webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally + } else { + isWaitingForRemoteSDP = true + } completion?() } @@ -133,4 +177,31 @@ public final class SessionCall: NSObject { hasEnded = true AppEnvironment.shared.callManager.reportCurrentCallEnded() } + + // MARK: Renderer + func attachRemoteVideoRenderer(_ renderer: RTCVideoRenderer) { + webRTCSession.attachRemoteRenderer(renderer) + } + + func attachLocalVideoRenderer(_ renderer: RTCVideoRenderer) { + webRTCSession.attachLocalRenderer(renderer) + } + + // MARK: Delegate + public func webRTCIsConnected() { + self.hasConnected = true + } + + public func isRemoteVideoDidChange(isEnabled: Bool) { + isRemoteVideoEnabled = isEnabled + } + + public func dataChannelDidOpen() { + // Send initial video status + if (isVideoEnabled) { + webRTCSession.turnOnVideo() + } else { + webRTCSession.turnOffVideo() + } + } } diff --git a/Session/Calls/CallVC+Camera.swift b/Session/Calls/CallVC+Camera.swift index 17dc4f196..ef440e2e4 100644 --- a/Session/Calls/CallVC+Camera.swift +++ b/Session/Calls/CallVC+Camera.swift @@ -9,6 +9,6 @@ extension CallVC : CameraManagerDelegate { let timestampNs = Int64(timestamp * 1000000000) let frame = RTCVideoFrame(buffer: rtcPixelBuffer, rotation: RTCVideoRotation._0, timeStampNs: timestampNs) frame.timeStamp = Int32(timestamp) - webRTCSession.handleLocalFrameCaptured(frame) + call.webRTCSession.handleLocalFrameCaptured(frame) } } diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index cf816f940..2d9a17afc 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -4,12 +4,9 @@ import SessionMessagingKit import SessionUtilitiesKit import UIKit -final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelegate { +final class CallVC : UIViewController, VideoPreviewDelegate { let call: SessionCall - var webRTCSession: WebRTCSession { return call.webRTCSession } var shouldAnswer = false - var isMuted = false - var isVideoEnabled = false var shouldRestartCamera = true weak var conversationVC: ConversationVC? = nil @@ -19,14 +16,10 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega return result }() - lazy var videoCapturer: RTCVideoCapturer = { - return RTCCameraVideoCapturer(delegate: webRTCSession.localVideoSource) - }() - // MARK: UI Components private lazy var localVideoView: RTCMTLVideoView = { let result = RTCMTLVideoView() - result.isHidden = !isVideoEnabled + result.isHidden = !call.isVideoEnabled result.contentMode = .scaleAspectFill result.set(.width, to: 80) result.set(.height, to: 173) @@ -98,7 +91,7 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega private lazy var switchCameraButton: UIButton = { let result = UIButton(type: .custom) - result.isEnabled = isVideoEnabled + result.isEnabled = call.isVideoEnabled let image = UIImage(named: "SwitchCamera")!.withTint(.white) result.setImage(image, for: UIControl.State.normal) result.set(.width, to: 60) @@ -161,13 +154,35 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega init(for call: SessionCall) { self.call = call super.init(nibName: nil, bundle: nil) - self.call.webRTCSession.delegate = self + setupStateChangeCallbacks() + self.modalPresentationStyle = .overFullScreen + self.modalTransitionStyle = .crossDissolve + } + + func setupStateChangeCallbacks() { + self.call.remoteVideoStateDidChange = { isEnabled in + DispatchQueue.main.async { + UIView.animate(withDuration: 0.25) { + self.remoteVideoView.alpha = isEnabled ? 1 : 0 + } + } + } + self.call.hasConnectedDidChange = { + DispatchQueue.main.async { + self.callInfoLabel.text = "Connected" + self.minimizeButton.isHidden = false + UIView.animate(withDuration: 0.5, delay: 1, options: [], animations: { + self.callInfoLabel.alpha = 0 + }, completion: { _ in + self.callInfoLabel.isHidden = true + self.callInfoLabel.alpha = 1 + }) + } + } self.call.hasEndedDidChange = { self.conversationVC?.showInputAccessoryView() self.presentingViewController?.dismiss(animated: true, completion: nil) } - self.modalPresentationStyle = .overFullScreen - self.modalTransitionStyle = .crossDissolve } required init(coder: NSCoder) { preconditionFailure("Use init(for:) instead.") } @@ -175,10 +190,9 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black - WebRTCSession.current = webRTCSession setUpViewHierarchy() if shouldRestartCamera { cameraManager.prepare() } - touch(videoCapturer) + touch(call.videoCapturer) titleLabel.text = self.call.contactName self.call.startSessionCall{ self.callInfoLabel.text = "Ringing..." @@ -197,12 +211,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega callInfoLabel.translatesAutoresizingMaskIntoConstraints = false callInfoLabel.center(in: view) // Remote video view - webRTCSession.attachRemoteRenderer(remoteVideoView) + call.attachRemoteVideoRenderer(remoteVideoView) view.addSubview(remoteVideoView) remoteVideoView.translatesAutoresizingMaskIntoConstraints = false remoteVideoView.pin(to: view) // Local video view - webRTCSession.attachLocalRenderer(localVideoView) + call.attachLocalVideoRenderer(localVideoView) view.addSubview(localVideoView) localVideoView.pin(.right, to: .right, of: view, withInset: -Values.smallSpacing) let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing @@ -252,45 +266,13 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if (isVideoEnabled && shouldRestartCamera) { cameraManager.start() } + if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() } shouldRestartCamera = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - if (isVideoEnabled && shouldRestartCamera) { cameraManager.stop() } - } - - // MARK: Delegate - func webRTCIsConnected() { - DispatchQueue.main.async { - self.callInfoLabel.text = "Connected" - self.call.hasConnected = true - self.minimizeButton.isHidden = false - UIView.animate(withDuration: 0.5, delay: 1, options: [], animations: { - self.callInfoLabel.alpha = 0 - }, completion: { _ in - self.callInfoLabel.isHidden = true - self.callInfoLabel.alpha = 1 - }) - } - } - - func isRemoteVideoDidChange(isEnabled: Bool) { - DispatchQueue.main.async { - UIView.animate(withDuration: 0.25) { - self.remoteVideoView.alpha = isEnabled ? 1 : 0 - } - } - } - - func dataChannelDidOpen() { - // Send initial video status - if (isVideoEnabled) { - webRTCSession.turnOnVideo() - } else { - webRTCSession.turnOffVideo() - } + if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() } } // MARK: Interaction @@ -349,13 +331,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega } @objc private func operateCamera() { - if (isVideoEnabled) { - webRTCSession.turnOffVideo() + if (call.isVideoEnabled) { localVideoView.isHidden = true cameraManager.stop() videoButton.alpha = 0.5 switchCameraButton.isEnabled = false - isVideoEnabled = false + call.isVideoEnabled = false } else { let previewVC = VideoPreviewVC() previewVC.delegate = self @@ -364,13 +345,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega } func cameraDidConfirmTurningOn() { - webRTCSession.turnOnVideo() localVideoView.isHidden = false cameraManager.prepare() cameraManager.start() videoButton.alpha = 1.0 switchCameraButton.isEnabled = true - isVideoEnabled = true + call.isVideoEnabled = true } @objc private func switchCamera() { @@ -378,14 +358,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega } @objc private func switchAudio() { - if isMuted { + if call.isMuted { switchAudioButton.backgroundColor = UIColor(hex: 0x1F1F1F) - isMuted = false - webRTCSession.unmute() + call.isMuted = false } else { switchAudioButton.backgroundColor = Colors.destructive - isMuted = true - webRTCSession.mute() + call.isMuted = true } } diff --git a/Session/Calls/Views & Modals/MiniCallView.swift b/Session/Calls/Views & Modals/MiniCallView.swift index c161264e4..4e991f996 100644 --- a/Session/Calls/Views & Modals/MiniCallView.swift +++ b/Session/Calls/Views & Modals/MiniCallView.swift @@ -38,7 +38,7 @@ final class MiniCallView: UIView { self.addSubview(background) background.pin(to: self) // Remote video view - callVC.webRTCSession.attachRemoteRenderer(remoteVideoView) + callVC.call.attachRemoteVideoRenderer(remoteVideoView) self.addSubview(remoteVideoView) remoteVideoView.translatesAutoresizingMaskIntoConstraints = false remoteVideoView.pin(to: self) diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 70c0976b5..2ca189647 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -6,23 +6,38 @@ import UIKit extension AppDelegate { // MARK: Call handling + func createNewIncomingCall(caller: String, uuid: String) { + let call = SessionCall(for: caller, uuid: uuid, mode: .answer) + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully + if let conversationVC = presentingVC as? ConversationVC, let contactThread = conversationVC.thread as? TSContactThread, contactThread.contactSessionID() == caller { + let callVC = CallVC(for: call) + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + presentingVC.present(callVC, animated: true, completion: nil) + } else { + call.reportIncomingCallIfNeeded{ error in + if let error = error { + SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") + let incomingCallBanner = IncomingCallBanner(for: call) + incomingCallBanner.show() + } + } + } + } + @objc func setUpCallHandling() { + // Pre offer messages + MessageReceiver.handlePreOfferCallMessage = { message in + guard CurrentAppContext().isMainApp else { return } + self.createNewIncomingCall(caller: message.sender!, uuid: message.uuid!) + } // Offer messages MessageReceiver.handleOfferCallMessage = { message in DispatchQueue.main.async { + guard let call = AppEnvironment.shared.callManager.currentCall, message.uuid == call.uuid.uuidString else { return } let sdp = RTCSessionDescription(type: .offer, sdp: message.sdps![0]) - let call = SessionCall(for: message.sender!, uuid: message.uuid!, mode: .answer(sdp: sdp)) - guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully - if let conversationVC = presentingVC as? ConversationVC, let contactThread = conversationVC.thread as? TSContactThread, contactThread.contactSessionID() == message.sender! { - let callVC = CallVC(for: call) - callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 - presentingVC.present(callVC, animated: true, completion: nil) - } else { - let incomingCallBanner = IncomingCallBanner(for: call) - incomingCallBanner.show() - } + call.didReceiveRemoteSDP(sdp: sdp) } } // Answer messages diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index b2d560b95..fe47fdf7e 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -238,7 +238,14 @@ public enum PushRegistrationError: Error { public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { owsAssertDebug(CurrentAppContext().isMainApp) owsAssertDebug(type == .voIP) - Vibration.shared.startVibration() + let payload = payload.dictionaryPayload + if let uuid = payload["uuid"] as? String, let caller = payload["caller"] as? String { + let appDelegate = UIApplication.shared.delegate as! AppDelegate + appDelegate.createNewIncomingCall(caller: caller, uuid: uuid) + appDelegate.startPollerIfNeeded() + appDelegate.startClosedGroupPoller() + appDelegate.startOpenGroupPollersIfNeeded() + } } } diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index 951dc1c35..f2d5cde4c 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -251,9 +251,9 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { print("[Calls] Peer connection should negotiate.") - Storage.write { transaction in - self.sendOffer(to: self.contactSessionID, using: transaction).retainUntilComplete() - } +// Storage.write { transaction in +// self.sendOffer(to: self.contactSessionID, using: transaction).retainUntilComplete() +// } } public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceConnectionState) { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index e70c9c70e..cede76d96 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -279,7 +279,9 @@ extension MessageReceiver { print("[Calls] Received pre-offer message.") if getWebRTCSession().uuid != message.uuid! { // TODO: Call in progress, put the new call on hold/reject + return } + handlePreOfferCallMessage?(message) case .offer: print("[Calls] Received offer message.") if getWebRTCSession().uuid != message.uuid! { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index b2cc26bc4..4d17a232f 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -2,6 +2,7 @@ import SessionUtilitiesKit public enum MessageReceiver { private static var lastEncryptionKeyPairRequest: [String:Date] = [:] + public static var handlePreOfferCallMessage: ((CallMessage) -> Void)? public static var handleOfferCallMessage: ((CallMessage) -> Void)? public static var handleAnswerCallMessage: ((CallMessage) -> Void)? public static var handleEndCallMessage: ((CallMessage) -> Void)? diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 8fccb96ec..c44649c60 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -5,7 +5,7 @@ import PromiseKit public final class PushNotificationAPI : NSObject { // MARK: Settings - public static let server = "https://live.apns.getsession.org" + public static let server = "https://dev.apns.getsession.org" public static let serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" private static let maxRetryCount: UInt = 4 private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index fc0ea2967..736bd7c82 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -96,7 +96,7 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension notificationContent.badge = 1 notificationContent.title = "Session" notificationContent.body = "\(senderDisplayName) is calling..." - return self.handleSuccessForIncomingCall(for: notificationContent, callID: callMessage.uuid!) + return self.handleSuccessForIncomingCall(for: notificationContent, callMessage: callMessage) default: return self.completeSilenty() } if (senderPublicKey == userPublicKey) { @@ -216,18 +216,21 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension contentHandler!(.init()) } - private func handleSuccessForIncomingCall(for content: UNMutableNotificationContent, callID: String) { - // TODO: poll for the real offer, play incoming call ring + private func handleSuccessForIncomingCall(for content: UNMutableNotificationContent, callMessage: CallMessage) { if #available(iOSApplicationExtension 14.5, *) { - CXProvider.reportNewIncomingVoIPPushPayload(["uuid": callID]) { error in - if let error = error { - owsFailDebug("Failed to notify main app of call message: \(error)") - } else { - Logger.info("Successfully notified main app of call message.") + if let uuid = callMessage.uuid, let caller = callMessage.sender { + let payload = ["uuid": uuid, "caller": caller] + return CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in + if let error = error { + owsFailDebug("Failed to notify main app of call message: \(error)") + } else { + Logger.info("Successfully notified main app of call message.") + } + self.completeSilenty() } - self.contentHandler!(content) } } + self.contentHandler!(content) } private func handleSuccess(for content: UNMutableNotificationContent) {