Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- //
- // CallManager.swift
- // CallKitExample
- //
- // Created by Thehe on 9/24/20.
- // Copyright © 2020 Vadax. All rights reserved.
- //
- import Foundation
- import UIKit
- import CallKit
- import AVFoundation
- import WebRTC
- private let sharedManager = CallManager.init()
- protocol CallManagerDelegate : AnyObject {
- func callDidAnswer()
- func callDidEnd()
- func callDidHold(isOnHold : Bool)
- func callDidFail()
- }
- class CallManager: NSObject, CXProviderDelegate {
- var provider : CXProvider?
- var callController : CXCallController?
- var currentCall : CallInfoModel?
- var callBackgroundHandlerIdentifier : UIBackgroundTaskIdentifier?
- weak var delegate : CallManagerDelegate?
- override init() {
- super.init()
- providerAndControllerSetup()
- NotificationCenter.default.addObserver(self, selector: #selector(callStateChanged(_:)), name: NSNotification.Name.VSLCallStateChanged, object: nil)
- }
- deinit {
- NotificationCenter.default.removeObserver(self, name: NSNotification.Name.VSLCallStateChanged, object: nil)
- }
- class var sharedInstance : CallManager {
- return sharedManager
- }
- //MARK: - Setup
- func providerAndControllerSetup() {
- // configuration.ringtoneSound = "Ringtone.caf"
- createProvider()
- }
- func createProvider(){
- // print("call manager createProvider")
- let configuration = CXProviderConfiguration.init(localizedName: "Link call manager")
- configuration.iconTemplateImageData = UIImage(named: "logo_square")?.pngData()
- configuration.supportsVideo = true
- configuration.maximumCallsPerCallGroup = 1
- configuration.maximumCallGroups = 1
- switch UserDefaults.standard.string(forKey: INCOMING_SOUND){
- case SOUND_DEFAULT:
- configuration.ringtoneSound = "Ringtone.caf"
- break
- case SOUND_PIANO:
- configuration.ringtoneSound = "sound.mp3"
- break
- default:
- break
- }
- configuration.supportedHandleTypes = [.emailAddress, .phoneNumber, .generic]
- provider = CXProvider.init(configuration: configuration)
- provider?.setDelegate(self, queue: nil)
- callController = CXCallController.init()
- }
- func reportIncomingCallFor(call: CallInfoModel) {
- if !isOnPhoneCalling(call: call) && !call.isSipCall(){
- configureAudioSessionToDefaultSpeaker()
- }
- let update = CXCallUpdate.init()
- update.hasVideo = (call.Video == 1)
- update.supportsDTMF = false
- update.supportsGrouping = false
- update.supportsUngrouping = false
- update.supportsHolding = false
- update.remoteHandle = CXHandle.init(type: .emailAddress, value: call.CallerDisplay ?? UNKNOWN)
- update.localizedCallerName = call.CallerDisplay
- weak var weakSelf = self
- provider!.reportNewIncomingCall(with: call.uuid, update: update, completion: { (error : Error?) in
- if error != nil {
- weakSelf?.delegate?.callDidFail()
- print("callDidFail :\(String(describing: error?.localizedDescription))")
- AppDelegate.shared.sendLog("callDidFail :\(String(describing: error?.localizedDescription))")
- }else {
- //discard call immediately to endsure call screen is not show
- if self.isOnPhoneCalling(call: call){
- AppDelegate.shared.sendLog("reject call \(call.toJSON()) because user in on calling")
- print("reject call \(call.uuid) because user in on calling")
- self.provider?.reportCall(with: call.uuid, endedAt: Date(), reason: .remoteEnded)
- // self.rejectCall(call)
- return
- }
- weakSelf?.currentCall = call
- print("add new call: \(call.uuid)")
- AppDelegate.shared.callAnswerTimer?.invalidate()
- AppDelegate.shared.callAnswerTimer = nil
- AppDelegate.shared.callAnswerTimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(self.hangUpOnCallTimeOut), userInfo: nil, repeats: false)
- if let roomJanusInfo = self.currentCall?.roomJanusInfo{
- guard let mediaRoomId = roomJanusInfo.MediaRoomId else { return }
- guard let roomPWd = roomJanusInfo.MediaRoomPassword else { return }
- guard let roomUrl = roomJanusInfo.MediaServerAddress else { return }
- var randomCallUUID = ""
- if let pushkit = UserDefaults.standard.string(forKey: PUSHKIT_TOKEN){
- randomCallUUID = String(pushkit.prefix(20))
- }else{
- randomCallUUID = UUID.init().uuidString
- }
- let senderName = "\(AppUtils.currentUser().Id)_\(randomCallUUID)"
- var stuns = [RTCIceServer]()
- if !roomJanusInfo.iceServers.isEmpty{
- let iceServers = roomJanusInfo.iceServers
- if iceServers.filter({ $0.urls!.contains("stun:") }).count > 0{
- let mapped = iceServers.filter{ $0.urls!.contains("stun:")}.map{ $0.urls!}
- mapped.forEach { (url) in
- stuns.append(RTCIceServer(urlStrings: [url]))
- }
- }
- if iceServers.filter({ $0.urls!.contains("turn:") }).count > 0{
- let mapped = iceServers.filter{ $0.urls!.contains("turn:")}
- stuns.append(contentsOf: mapped.map{
- RTCIceServer(urlStrings:[$0.urls!],
- username:$0.username!,
- credential:$0.credential!)
- })
- }
- }
- if stuns.isEmpty{
- stuns.append(RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"]))
- }
- let roomManager = JanusRoomManager.shared
- roomManager.enableDataChannel = true
- roomManager.initRoom(roomID: mediaRoomId.intValue, roomUrl: roomUrl, roomPwd: roomPWd, senderName: senderName, iceServers: stuns, enableVideo : self.currentCall?.Video == 1, enableAudio: true, isSendAudio: true)
- roomManager.enableDataChannel = true
- if self.currentCall?.Video != 1{
- roomManager.isSingleCallWithVoiceOnly = true
- }
- roomManager.preConnect = true
- AppDelegate.shared.sendLog("start Janus while get incoming call")
- roomManager.connect()
- }
- }
- })
- }
- @objc func hangUpOnCallTimeOut(){
- AppDelegate.shared.callAnswerTimer?.invalidate()
- AppDelegate.shared.callAnswerTimer = nil
- if let c = currentCall{
- if !c.isAccepted{
- print("reject this call due to timeout(30s)")
- self.provider?.reportCall(with: c.uuid, endedAt: Date(), reason: .failed)
- if !c.isSipCall(){
- AppDelegate.shared.sendLog("reject this call due to timeout(30s)")
- rejectCall(c, status: Int(AX_ROOM_CALLING_ANSWER_TimeOut)!)
- }else{
- }
- }
- }
- }
- func isOnPhoneCalling(call: CallInfoModel) -> Bool{
- if AppDelegate.shared.isCalling{
- AppDelegate.shared.sendLog("isOnPhoneCalling true -> AppDelegate.shared.isCalling")
- return true
- }
- //ignore show callkit when user is in calling
- if call.Calling == Int(AX_CALL_TYPE_START) {
- if PIPKit.isActive {
- AppDelegate.shared.sendLog("isOnPhoneCalling true -> PIPKit.isActive")
- return true
- }
- if let _ = AppDelegate.shared.window?.rootViewController?.topMostViewController() as? MainVC {
- AppDelegate.shared.sendLog("isOnPhoneCalling true -> topMostViewController() as? MainVC")
- return true
- }
- if let _ = AppDelegate.shared.window?.rootViewController?.topMostViewController() as? IPPhoneCallScreen {
- AppDelegate.shared.sendLog("isOnPhoneCalling true -> topMostViewController() as? IPPhoneCallScreen")
- return true
- }
- if let _ = AppDelegate.shared.window?.rootViewController?.topMostViewController() as? SingleCallVC {
- AppDelegate.shared.sendLog("isOnPhoneCalling true -> topMostViewController() as? SingleCallVC")
- return true
- }
- var isCallingGroup = false
- if let chatVC = AppDelegate.shared.window?.rootViewController?.topMostViewController() as? ChatVC {
- if chatVC.children.contains(where: { $0 is MainVC}){
- isCallingGroup = true
- }
- }
- guard !isCallingGroup else {
- AppDelegate.shared.sendLog("isOnPhoneCalling true -> isCallingGroup")
- return true
- }
- }
- return false
- }
- func isOnSinglePhoneCalling(call: CallInfoModel) -> Bool{
- if AppDelegate.shared.isCalling{
- return true
- }
- //ignore show callkit when user is in calling
- if call.Calling == Int(AX_CALL_TYPE_START) {
- if PIPKit.isActive {
- return true
- }
- if let _ = AppDelegate.shared.window?.rootViewController?.topMostViewController() as? IPPhoneCallScreen {
- return true
- }
- if let _ = AppDelegate.shared.window?.rootViewController?.topMostViewController() as? SingleCallVC {
- return true
- }
- if let _ = AppDelegate.shared.window?.rootViewController?.topMostViewController() as? IPPhoneCallScreen {
- return true
- }
- }
- return false
- }
- func startCallWithPhoneNumber(call : CallInfoModel) {
- configureAudioSessionToDefaultSpeaker()
- currentCall = call
- if let unwrappedCurrentCall = currentCall {
- let handle = CXHandle.init(type: .generic, value: unwrappedCurrentCall.CallerDisplay ?? UNKNOWN)
- let startCallAction = CXStartCallAction.init(call: unwrappedCurrentCall.uuid, handle: handle)
- let transaction = CXTransaction.init()
- transaction.addAction(startCallAction)
- requestTransaction(transaction: transaction)
- self.provider?.reportOutgoingCall(with: startCallAction.callUUID, startedConnectingAt: nil)
- }
- }
- func endCall() {
- if let unwrappedCurrentCall = currentCall {
- print("Call manager end call: \(unwrappedCurrentCall.uuid)")
- let endCallAction = CXEndCallAction.init(call: unwrappedCurrentCall.uuid)
- let transaction = CXTransaction.init()
- transaction.addAction(endCallAction)
- requestTransaction(transaction: transaction)
- }
- }
- func endCall(uuid : UUID){
- let endCallAction = CXEndCallAction.init(call: uuid)
- let transaction = CXTransaction.init()
- transaction.addAction(endCallAction)
- requestTransaction(transaction: transaction)
- }
- func holdCall(hold : Bool) {
- if let unwrappedCurrentCall = currentCall {
- let holdCallAction = CXSetHeldCallAction.init(call: unwrappedCurrentCall.uuid, onHold: hold)
- let transaction = CXTransaction.init()
- transaction.addAction(holdCallAction)
- requestTransaction(transaction: transaction)
- }
- }
- func requestTransaction(transaction : CXTransaction) {
- weak var weakSelf = self
- callController?.request(transaction, completion: { (error : Error?) in
- if error != nil {
- print("requestTransaction error \(String(describing: error?.localizedDescription))")
- weakSelf?.delegate?.callDidFail()
- // if let call = weakSelf?.currentCall{
- // print("trying to mark call ended")
- // self.provider?.reportCall(with: call.uuid, endedAt: Date(), reason: .remoteEnded)
- // }
- }
- print("requestTransaction end call success")
- })
- }
- //MARK : - CXProviderDelegate
- func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
- print("call manager CXStartCallAction")
- if let currentCall = currentCall{
- if currentCall.isSipCall(){
- print("start handle sip outgoing call")
- //ipcall here
- let callManager = VialerSIPLib.sharedInstance().callManager
- if let call = VialerSIPLib.sharedInstance().callManager.call(with: action.callUUID){
- callManager.audioController.configureAudioSession()
- call.start { error in
- if let error = error{
- print("error start ip call: \(error)")
- action.fail()
- return
- }
- print("start call success")
- let notificationInfo = [VSLNotificationUserInfoCallKey : call]
- NotificationCenter.default.post(name: NSNotification.Name.CallKitProviderDelegateOutboundCallStarted, object: AppDelegate.shared.providerDelegate, userInfo: notificationInfo)
- }
- }
- action.fulfill()
- }else{
- print("start link outgoing call")
- action.fulfill()
- }
- }else{
- action.fail()
- }
- }
- func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
- AppDelegate.shared.callAnswerTimer?.invalidate()
- AppDelegate.shared.callAnswerTimer = nil
- //todo: answer network call
- delegate?.callDidAnswer()
- if currentCall?.Video == 1{
- if SPPermission.camera.isDenied || SPPermission.microphone.isDenied{
- rejectCall(currentCall)
- self.currentCall = nil
- action.fail()
- AppDelegate.shared.sendNotify(Language.get("Can not accept call"), Language.get("Please accept microphone and camera permission to fully use our call service"), [:])
- return
- }
- }else{
- if SPPermission.microphone.isDenied{
- rejectCall(currentCall)
- self.currentCall = nil
- action.fail()
- AppDelegate.shared.sendNotify(Language.get("Can not accept call"), Language.get("Please accept microphone and camera permission to fully use our call service"), [:])
- return
- }
- }
- currentCall?.isAccepted = true
- if let sipAccount = currentCall?.sipAccount, !sipAccount.isEmpty{
- let sb = UIStoryboard(name: "other", bundle: nil)
- let vc = sb.instantiateViewController(withIdentifier: "IPPhoneCallScreen") as! IPPhoneCallScreen
- vc.modalPresentationStyle = .fullScreen
- vc.callObj = currentCall
- let appDelegate = AppDelegate.shared
- if let topVC = appDelegate.window?.rootViewController?.topMostViewController(){
- topVC.present(vc, animated: true, completion: nil)
- }else{
- appDelegate.window?.rootViewController?.present(vc, animated: true, completion: nil)
- }
- }else{
- let sb = UIStoryboard(name: "main", bundle: nil)
- let vc = sb.instantiateViewController(withIdentifier: "SingleCallVC") as! SingleCallVC
- vc.modalPresentationStyle = .fullScreen
- vc.callObj = currentCall
- vc.isIncoming = true
- //when app opened from call, the keyboard height not working because window is nil + app is not running to calculate keyboardheight
- let height = UserDefaults.standard.integer(forKey: KEYBOARD_SIZE)
- if Keyboard.height() == 0{
- Keyboard.measuredSize = CGRect(0, 0 , Int(UIScreen.main.bounds.width), height)
- }
- PIPKit.show(with: vc)
- }
- action.fulfill()
- }
- func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
- //todo: configure audio session
- //todo: answer network call
- //stop timer
- AppDelegate.shared.callAnswerTimer?.invalidate()
- AppDelegate.shared.callAnswerTimer = nil
- print("Call manager CXEndCallAction")
- guard let currentCall = currentCall else {
- action.fail()
- return
- }
- if currentCall.isSipCall(){
- print("end sip outgoing call")
- guard let sipCallUUID = currentCall.sipUUID else {
- action.fail()
- return
- }
- if let call = VialerSIPLib.sharedInstance().callManager.call(with: sipCallUUID){
- if call.callState == .incoming{
- do{
- try call.decline()
- action.fulfill()
- }catch{
- action.fail()
- print("decline sip incoming call error : \(error.localizedDescription)")
- }
- }else{
- do{
- try call.hangup()
- action.fulfill()
- }catch{
- action.fail()
- print("hangup sip incoming call error : \(error.localizedDescription)")
- }
- }
- }else{
- print("can not find sip incomming call with uuid: \(action.callUUID)")
- action.fail()
- }
- self.currentCall = nil
- }else{
- print("end link incomming call")
- if !currentCall.isAccepted{
- rejectCall(currentCall)
- let roomManager = JanusRoomManager.shared
- roomManager.reset()
- roomManager.disconnect()
- }else{
- //NOTE: While calling we use signalr message to end call,fcm push notification is only to end displaying call
- //But in lock screen press button can trigger this method
- //end call while calling -> send did end to singlecallvc
- if PIPKit.isActive{
- if let _ = PIPKit.rootViewController as? SingleCallVC{
- delegate?.callDidEnd()
- }
- }
- }
- self.currentCall = nil
- action.fulfill()
- }
- }
- func configureRtcAudioSession(){
- // RTCDispatcher.dispatchAsync(on: RTCDispatcherQueueType.typeAudioSession) {
- // let audioSession = RTCAudioSession.sharedInstance()
- // audioSession.lockForConfiguration()
- // let configuration = RTCAudioSessionConfiguration.webRTC()
- // configuration.categoryOptions = [.allowBluetoothA2DP,.duckOthers, .allowBluetooth]
- // try? audioSession.setConfiguration(configuration)
- // audioSession.unlockForConfiguration()
- // }
- }
- func configureAudioSessionToDefaultSpeaker() {
- let session = AVAudioSession.sharedInstance()
- do{
- try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .mixWithOthers, .allowAirPlay, .allowBluetooth, .allowBluetoothA2DP])
- } catch {
- print("========== Error in setting category \(error.localizedDescription)")
- }
- do{
- // try session.setMode(.voiceChat)
- // check if currentRoute is set to a bluetooth audio device
- let btOutputTypes: [AVAudioSession.Port] = [.bluetoothHFP, .bluetoothA2DP, .bluetoothLE]
- let btOutputs = AVAudioSession.sharedInstance().currentRoute.outputs.filter { btOutputTypes.contains($0.portType) }
- // if so, set preferred audio input to built-in mic
- if !btOutputs.isEmpty {
- let builtInMicInput = AVAudioSession.sharedInstance().availableInputs?.filter { $0.portType == .builtInMic }.first
- try AVAudioSession.sharedInstance().setPreferredInput(builtInMicInput)
- } else {
- // set default input
- try AVAudioSession.sharedInstance().setPreferredInput(nil)
- }
- }catch {
- print("======== Error setting mode \(error.localizedDescription)")
- }
- do {
- if #available(iOS 13.0, *) {
- try session.setAllowHapticsAndSystemSoundsDuringRecording(true)
- } else {
- }
- } catch {
- print("======== Error setting haptic \(error.localizedDescription)")
- }
- do {
- try session.setPreferredSampleRate(44100.0)
- } catch {
- print("======== Error setting rate \(error.localizedDescription)")
- }
- do {
- try session.setPreferredIOBufferDuration(0.005)
- } catch {
- print("======== Error IOBufferDuration \(error.localizedDescription)")
- }
- do {
- try session.setActive(true)
- } catch {
- print("========== Error starting session \(error.localizedDescription)")
- }
- }
- func markCallIsConnecting(){
- guard let currentCall = currentCall else { return }
- provider?.reportOutgoingCall(with: currentCall.uuid, startedConnectingAt: nil)
- }
- func markCallIsConnected(){
- guard let currentCall = currentCall else { return }
- provider?.reportOutgoingCall(with: currentCall.uuid, connectedAt: nil)
- }
- func rejectCall(_ callObj : CallInfoModel?, status : Int = Int(AX_ROOM_CALLING_ANSWER_Decline)!){
- guard let callObj = callObj else { return }
- guard let serverId = callObj.RoomServerId else { return }
- guard let roomId = callObj.RoomId else { return }
- guard let callId = callObj.CallId else { return }
- let url = "\(AppUtils.baseUrl(serverId: serverId))\(UPDATE_P2p_CALL_STATUS)"
- let params = ["SessionId" : UserDefaults.standard.string(forKey: PUSHKIT_TOKEN)?.prefix(20) ?? UNKNOWN,
- "RoomId" : roomId,
- "CallId" : callId,
- "Status" : status,
- "ClientType" : 4000] as [String : Any]
- NWUtils.shared.postData(url: url, params: params) { (response) in
- print("reject call success \(response)")
- if self.currentCall?.CallerId == callObj.CallerId{
- self.currentCall = nil
- }
- } fail: { (error) in
- print("reject call fail \(error as! String)")
- if self.currentCall?.CallerId == callObj.CallerId{
- self.currentCall = nil
- }
- }
- }
- func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
- guard let currentCall = currentCall else {
- action.fail()
- return
- }
- if currentCall.isSipCall(){
- let callManager = VialerSIPLib.sharedInstance().callManager
- if let call = VialerSIPLib.sharedInstance().callManager.call(with: action.callUUID){
- do{
- try call.toggleHold()
- call.onHold ? callManager.audioController.deactivateAudioSession() : callManager.audioController.activateAudioSession()
- action.fulfill()
- }catch{
- action.fail()
- print("sip call hold failed: \(error.localizedDescription)")
- }
- }else{
- action.fail()
- }
- }else{
- action.fulfill()
- endCall()
- }
- }
- func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
- guard let currentCall = currentCall, let sipCallUUID = currentCall.sipUUID else {
- action.fail()
- return
- }
- if currentCall.isSipCall(){
- if let call = VialerSIPLib.sharedInstance().callManager.call(with: sipCallUUID){
- do{
- try call.toggleMute()
- action.fulfill()
- }catch{
- action.fail()
- print("sip call mute failed: \(error.localizedDescription)")
- }
- }else{
- action.fail()
- }
- }else{
- action.fulfill()
- endCall()
- }
- }
- func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
- }
- func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
- guard let currentCall = currentCall, let sipCallUUID = currentCall.sipUUID else {
- action.fail()
- return
- }
- if currentCall.isSipCall(){
- if let call = VialerSIPLib.sharedInstance().callManager.call(with: sipCallUUID){
- do{
- try call.sendDTMF(action.digits)
- action.fulfill()
- }catch{
- action.fail()
- print("sip call send dtmf failed: \(error.localizedDescription)")
- }
- }else{
- action.fail()
- }
- }else{
- action.fulfill()
- endCall()
- }
- }
- // Called when an action was not performed in time and has been inherently failed. Depending on the action, this timeout may also force the call to end. An action that has already timed out should not be fulfilled or failed by the provider delegate
- func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
- // React to the action timeout if necessary, such as showing an error UI.
- }
- /// Called when the provider's audio session activation state changes.
- func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
- print("callmanager audioSessionDidActivate")
- if let currentCall = currentCall, currentCall.isSipCall(){
- // VialerSIPLib.sharedInstance().callManager.audioController.activateAudioSession()
- }
- }
- func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
- print("callmanager audioSessionDidDeactivate")
- if let currentCall = currentCall, currentCall.isSipCall(){
- // VialerSIPLib.sharedInstance().callManager.audioController.deactivateAudioSession()
- }
- }
- func providerDidReset(_ provider: CXProvider) {
- print("providerDidReset")
- if let currentCall = currentCall, currentCall.isSipCall(){
- VialerSIPLib.sharedInstance().callManager.endAllCalls()
- }
- }
- @objc func callStateChanged(_ notification: Notification){
- guard let call = notification.userInfo?[VSLNotificationUserInfoCallKey] as? VSLCall else { return }
- print("callStateChanged: \(call.callState.rawValue)")
- guard let currentCall = currentCall else { return }
- //report based on call UUID , not sip uuid
- if call.callState == .calling{
- if !call.isIncoming{
- provider?.reportOutgoingCall(with: currentCall.uuid, startedConnectingAt: Date())
- }
- }else if call.callState == .early{
- if !call.isIncoming{
- provider?.reportOutgoingCall(with: currentCall.uuid, startedConnectingAt: Date())
- }
- }else if call.callState == .connecting{
- if !call.isIncoming{
- provider?.reportOutgoingCall(with: currentCall.uuid, startedConnectingAt: Date())
- }
- }else if call.callState == .confirmed{
- if !call.isIncoming{
- provider?.reportOutgoingCall(with: currentCall.uuid, connectedAt: Date())
- }
- }else if call.callState == .disconnected{
- if !call.connected{
- provider?.reportOutgoingCall(with: currentCall.uuid, connectedAt: Date())
- provider?.reportCall(with: currentCall.uuid, endedAt: Date(), reason: .answeredElsewhere)
- }else if !call.userDidHangUp{
- provider?.reportCall(with: currentCall.uuid, endedAt: Date(), reason: .remoteEnded)
- }
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement