refactoring for CallKit

pull/560/head
ryanzhao 4 years ago
parent e530b50938
commit 6f78d6dfbe

@ -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,16 +125,23 @@ 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
}
}
// MARK: Actions
@ -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
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()
}
}
}

@ -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)
}
}

@ -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
}
}

@ -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)

@ -6,25 +6,40 @@ import UIKit
extension AppDelegate {
// MARK: Call handling
@objc func setUpCallHandling() {
// Offer messages
MessageReceiver.handleOfferCallMessage = { message in
DispatchQueue.main.async {
let sdp = RTCSessionDescription(type: .offer, sdp: message.sdps![0])
let call = SessionCall(for: message.sender!, uuid: message.uuid!, mode: .answer(sdp: sdp))
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() == message.sender! {
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])
call.didReceiveRemoteSDP(sdp: sdp)
}
}
// Answer messages
MessageReceiver.handleAnswerCallMessage = { message in
DispatchQueue.main.async {

@ -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()
}
}
}

@ -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) {

@ -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! {

@ -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)?

@ -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

@ -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,19 +216,22 @@ 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 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.contentHandler!(content)
self.completeSilenty()
}
}
}
self.contentHandler!(content)
}
private func handleSuccess(for content: UNMutableNotificationContent) {
contentHandler!(content)

Loading…
Cancel
Save