How to set timeout for CallKit incoming call UI - ios

I'm developing Video call feature in my app and using CallKit to be the incoming call UI. And i found an edge case like:
User A: call user B
User B:
The app is in terminated state. And CallKit incoming UI shows for user B.
User B doesn't notice (since silent mode) and let the incoming UI keep showing
User A doesn't end the call; or for some reasons, user A lost internet or quit the app (therefore my server doesn't send Cancel command via VoIP notification) so there's no way for user B to end the incoming UI until user B touch Cancel or Answer
So is there any way to set a timeout for an incoming UI of CallKit? For example: If i set the timeout is 60 seconds then the incoming UI just shows in 60 seconds the auto dismiss.
Here is my code to show the incoming UI:
let update = CXCallUpdate()
update.localizedCallerName = callerName
update.remoteHandle = CXHandle(type: .phoneNumber, value: myID)
update.hasVideo = true
self.provider.reportNewIncomingCall(with: uuid, update: update) { [weak self] error in
guard let self = self else { return }
if error == nil {
// Store my calls
let call = Call(uuid: uuid, handle: handle)
self.callKitManager.add(call: call)
}
}
Any help would be greatly appreciated. Thanks.

There's no way to set a timeout using the CallKit API. You just have to implement it by yourself.
In the class where you handle all the call logic, you should add something like the following:
private func startRingingTimer(for call: Call)
{
let vTimer = Timer(
timeInterval: 60,
repeats: false,
block: { [weak self] _ in
self?.ringingDidTimeout(for: call)
})
vTimer.tolerance = 0.5
RunLoop.current.add(vTimer, forMode: .common)
ringingTimer = vTimer
}
private func ringingDidTimeout(for call: Call)
{
...
self.provider.reportCall(with: call.uuid, endedAt: nil, reason: .unanswered)
...
}
Then you should call startRingingTimer(for: call) as soon as you successfully reported a new incoming call; and, of course, you have to invalidate the timer if the user answers the call.

Related

How to end a CallKit call from custom UI without having caller UUID?

I working on a video call application in iOS Swift 5, where call can only initiate from the backend web app and the mobile app can only answer to those calls,. Means there is no mobile-mobile communication, communication between web app and mobile app. In my application I'm using PushKit with CallKit for notifying the incoming call in the background or killed state. So in the background or killed state, if I get an incoming call, it will show the calling screen using CallKit and if I pressed Answer button, it will navigate to my own custom video call screen where I have end button for dismissing the call. But when I press the end button, the VoIP call get disconnected. But the CallKit call is not dismissing(Still show the green bar on the top of Homescreen). And I checked for how ending the CallKit call via code, but in most of the solution the CallKit is dismissing with the use of callUUID. But I don't know from where I will get that UUID. In some code I saw the UUID is received from the push payload, but in my case the call is initiated from the web app, so I'm not receiving the caller UUID. Please help me.
This is my code,
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
print(payload.dictionaryPayload)
let state = UIApplication.shared.applicationState
if state == .background || state == .inactive {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: callerName)
update.hasVideo = true
provider.reportNewIncomingCall(with: UUID(), update: update, completion: { error in })
} else if state == .active {
// no need for calling screen
}
}
And I tired the following code to end the call, but not working.
func endCall(call: UUID) {
let callController = CXCallController()
let endCallAction = CXEndCallAction(call: call)
let transaction = CXTransaction(action: endCallAction)
callController.request(transaction) { error in
if let error = error {
print("EndCallAction transaction request failed: \(error.localizedDescription).")
self.provider.reportCall(with: call, endedAt: Date(), reason: .remoteEnded)
return
}
print("EndCallAction transaction request successful")
}
Here I'm passing call as current UUID, then I'm getting error response as
EndCallAction transaction request failed: The operation couldn’t be
completed.
The uuid value is one that you provide.
You are providing it here:
provider.reportNewIncomingCall(with: UUID(),...
Because you are simply allocating a new UUID and passing it directly in reportNewIncomingCall you don't know the uuid when you need it later.
You need to store this uuid in a property so that you can provide it to your endCall function.

iOS 14 How to trigger Local Network dialog and check user answer?

I've seen this Q/A What triggers, but it's not what I want. I also read this Network privacy permission check, but there is no answer. I also search for any methods or classes which can help me here: Network, but no luck again.
There is a new dialog for the Local Network authorization, where user can Allow/Don't Allow "to find and connect to devices on your local network".
But I'm struggling to find any API for how to trigger this popup and how to check is access granted or not(for example in AVCapture, I can check the authorization status for AVMediaType).
Thank you!
I found a way to trigger the prompt, receive a callback of the user's selection, and detect if the user has previously allowed or denied the prompt if it already appeared.
To trigger the permission we use a service discovery API. When the user declines or previously declined we receive an error.
It doesn't indicate if the permission was granted, so we also published a network service that returns success if the permission has been granted.
By combining the 2 into a single component, we can trigger the prompt and get an indication of approval or decline: Until we receive success from the network service or error from the service discovery we assume that the permission is still pending.
import Foundation
import Network
#available(iOS 14.0, *)
public class LocalNetworkAuthorization: NSObject {
private var browser: NWBrowser?
private var netService: NetService?
private var completion: ((Bool) -> Void)?
public func requestAuthorization(completion: #escaping (Bool) -> Void) {
self.completion = completion
// Create parameters, and allow browsing over peer-to-peer link.
let parameters = NWParameters()
parameters.includePeerToPeer = true
// Browse for a custom service type.
let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters)
self.browser = browser
browser.stateUpdateHandler = { newState in
switch newState {
case .failed(let error):
print(error.localizedDescription)
case .ready, .cancelled:
break
case let .waiting(error):
print("Local network permission has been denied: \(error)")
self.reset()
self.completion?(false)
default:
break
}
}
self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
self.netService?.delegate = self
self.browser?.start(queue: .main)
self.netService?.publish()
}
private func reset() {
self.browser?.cancel()
self.browser = nil
self.netService?.stop()
self.netService = nil
}
}
#available(iOS 14.0, *)
extension LocalNetworkAuthorization : NetServiceDelegate {
public func netServiceDidPublish(_ sender: NetService) {
self.reset()
print("Local network permission has been granted")
completion?(true)
}
}
How to use:
Add LocalNetworkAuthorization class to your project
Open .plist file and add "_bonjour._tcp", "_lnp._tcp.", as a values under "Bonjour services"
Call requestAuthorization() to trigger the prompt or get the authorization status if it already been approved/denied
I did open DTS request and had conversion with Apple support team. Here is some important parts which I included below.
How to check is access granted or not
From support team:
For know, there is no such an API to check user permission.
From support team:
If the user declines, the connection fails. Exactly how it fails
depends on the network API you’re using and how you use that API.
By default the connection will fail with NSURLErrorNotConnectedToInternet.
If you set waitsForConnectivity on the session configuration, the request will wait for things to improve. In that case you’ll receive
the -URLSession:taskIsWaitingForConnectivity: delegate callback to
tell you about this. If the user changes their mind and enables local
network access, the connection will then go through.
Unfortunately there’s no direct way to determine if this behaviour is
the result of a local network privacy restriction or some other
networking failure.
How to trigger this popup
From support team:
the problem here is that the local network permission alert is
triggered by outgoing traffic and you do not generate any outgoing
traffic. The only way around this is to generate some dummy outgoing
traffic in order to trigger this alert.
I’ve seen other developers in this situation and the absence of a
direct API to trigger the local network permission alert is quite
annoying. I encourage you to file a bug about this.
I’ve been discussing this issue with the local network privacy team
and our current advice for apps in your situation — that is, apps that
want to receive broadcasts but don’t send any local network traffic —
is as follows:
The system should do a better job of handling this. We’re tracking that as a bug rdar://problem/67975514. This isn’t fixed in the
current iOS 14.2b1 release but you should continue to test with iOS
beta seeds as they are released.
In the meantime you can force the local network privacy alert to show by sending a message. We specifically recommend that you send a
message that’s roughly equivalent to the message you’re trying to
receive, so in your case that means sending an IPv4 UDP broadcast.
UPDATE
For iOS 14.2 - prompt is received for inbound traffic FIXED. Because of this you don't need below example for simulating traffic to triggering prompt.
Here is class for dummy outgoing traffic simulation:
example
That traffic will never leave the iOS device and thus, even if the
interface is asleep, it won’t wake it up. And even if it did wake up
the interface, the cost of that is trivial because you’re not doing it
over and over again, just once in order to trigger the local network
privacy alert.
In my case it was accessing this variable for some internal device statistics:
ProcessInfo.processInfo.hostName
Accessing this variable caused the alert to appear. If it doesn't cover your case perhaps you can search source code for some references around the local network/host.
Because there is no API that directly returns your local network access state you can use next approach with publishing your Bonjour service and it returns the right result if access to local network was already set for your app (on app start e.g.). The approach causes the alert to appear as well but returns false before you select any button so to get the right result you should put this check to applicationDidBecomeActive and it will give the correct state after local network alert is disappeared and you return to your app.
class getLocalNetworkAccessState : NSObject {
var service: NetService
var denied: DispatchWorkItem?
var completion: ((Bool) -> Void)
#discardableResult
init(completion: #escaping (Bool) -> Void) {
self.completion = completion
service = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
super.init()
denied = DispatchWorkItem {
self.completion(false)
self.service.stop()
self.denied = nil
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: denied!)
service.delegate = self
self.service.publish()
}
}
extension getLocalNetworkAccessState : NetServiceDelegate {
func netServiceDidPublish(_ sender: NetService) {
denied?.cancel()
denied = nil
completion(true)
}
func netService(_ sender: NetService, didNotPublish errorDict: [String : NSNumber]) {
print("Error: \(errorDict)")
}
}
How to use:
getLocalNetworkAccessState { granted in
print(granted ? "granted" : "denied")
}
NOTE: Don't forget to set NSLocalNetworkUsageDescription and add "_lnp._tcp." to NSBonjourServices in your Info.plist.
UPDATE
There is the second approach that works similar the code from above but can wait for an user's answer by checking an application state and then returns a valid access state for Local Network Privacy:
class LocalNetworkPrivacy : NSObject {
let service: NetService
var completion: ((Bool) -> Void)?
var timer: Timer?
var publishing = false
override init() {
service = .init(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
super.init()
}
#objc
func checkAccessState(completion: #escaping (Bool) -> Void) {
self.completion = completion
timer = .scheduledTimer(withTimeInterval: 2, repeats: true, block: { timer in
guard UIApplication.shared.applicationState == .active else {
return
}
if self.publishing {
self.timer?.invalidate()
self.completion?(false)
}
else {
self.publishing = true
self.service.delegate = self
self.service.publish()
}
})
}
deinit {
service.stop()
}
}
extension LocalNetworkPrivacy : NetServiceDelegate {
func netServiceDidPublish(_ sender: NetService) {
timer?.invalidate()
completion?(true)
}
}
// How to use
LocalNetworkPrivacy().checkAccessState { granted in
print(granted)
}
ObjC
You can use swift code without rewriting to ObjC and to do that just add swift file to your project and call checkAccessState directly (the function must be marked with #objc):
#import "YourProjectName-Swift.h" // import swift classes to objc
...
LocalNetworkPrivacy *local = [LocalNetworkPrivacy new];
[local checkAccessStateWithCompletion:^(BOOL granted) {
NSLog(#"Granted: %#", granted ? #"yes" : #"no");
}];
Apple has (late September 2020) published a Local Network Privacy FAQ which answers this, although it does seem that further changes to make this easier are likely.
There are Swift and Objective-C code examples for how to trigger the prompt by a workaround:
Currently there is no way to explicitly trigger the local network
privacy alert (r. 69157424). However, you can bring it up implicitly
by sending dummy traffic to a local network address. The code below
shows one way to do this. It finds all IPv4 and IPv6 addresses
associated with broadcast-capable network interfaces and sends a UDP
datagram to each one. This should trigger the local network privacy
alert, assuming the alert hasn’t already been displayed for your app.
And as for how to check result, keep your eye on this FAQ answer which says:
If your goal is to connect to a local network address using
NWConnection then, starting with iOS 14.2 beta, you can use the
unsatisfied reason property.
It can be triggered by sending dummy request with TCP IP socket. This code works perfectly for Flutter iOS app using Socket and IP address of the device itself:
import 'package:network_info_plus/network_info_plus.dart';
import 'dart:io';
try{
var deviceIp = await NetworkInfo().getWifiIP();
Duration? timeOutDuration = Duration(milliseconds: 100);
await Socket.connect(deviceIp, 80, timeout: timeOutDuration);
} catch (e) {
print(
'Exception..');
}
Another workaround to consider if you're making a local network request with URLSession and would like the request to wait for the user to consent to the dialog is to set the waitsForConnectivity flag of URLSessionConfiguration to true:
Find:
URLSession.shared.dataTask(...)
Replace with:
// Default config
let config = URLSessionConfiguration.default
// Wait for user to consent to local network access
if #available(iOS 11.0, *) {
config.waitsForConnectivity = true
}
// Execute network request
let task = URLSession(configuration: config).dataTask(...)
This will cause the request to hang until the dialog is either accepted or declined.
This works on (at least) iOS 16.
First create a MCNearbyServiceAdvertiser and MCNearbyServiceBrowser. Then, the popup appears when you start these 'services'; see start() in the code below.
Perhaps starting one of the too is sufficient too; I simply did both together, because that's what I needed.
class Connector : NSObject, ObservableObject
{
#Published var peers = [MCPeerID]()
#Published var event: String?
private let serviceType = "app"
private let peerId = MCPeerID(displayName: UIDevice.current.name)
private let serviceAdvertiser: MCNearbyServiceAdvertiser
private let serviceBrowser: MCNearbyServiceBrowser
private let session: MCSession
private let log = Logger()
override init()
{
session = MCSession(peer: peerId, securityIdentity: nil, encryptionPreference: .none)
serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerId,
discoveryInfo: ["event" : "hello"],
serviceType: serviceType)
serviceBrowser = MCNearbyServiceBrowser(peer: peerId, serviceType: serviceType)
super.init()
session.delegate = self
serviceAdvertiser.delegate = self
serviceBrowser.delegate = self
}
deinit
{
serviceAdvertiser.stopAdvertisingPeer()
serviceBrowser.stopBrowsingForPeers()
}
func start()
{
serviceAdvertiser.startAdvertisingPeer()
serviceBrowser.startBrowsingForPeers()
}
}
See a bit more code here
See other answers here for how to check if the user granted this permission.
For reference, my solution to that problem is here. It is optimized for the use with SwiftUI where it can be used as an ObservableObject.
It bases on #TalSahar's approach, but also retries publishing the net service when it fails. This covers the case of the user granting access while the app is running:
browser.stateUpdateHandler = { [weak self] state in
os_log("NWBrowser status update: %#", log: OSLog.default, type: .debug, "\(state)")
guard let self else { return }
switch state {
case .failed(_):
self.service?.publish()
case .waiting(_):
self.status = .denied
default:
break
}
}
I wrote up this class that can be used if you're not on iOS 14.2.
This class will prompt user for permission to access local network (first time).
Verify existing permission state if already denied/granted.
Just remember this instance has to be kept alive so if you are using this in a function call within another class you need to keep the instance alive outside of the scope of the calling function. You will also need the network multicasting entitlement under certain circumstances.
import UIKit
import Network
class LocalNetworkPermissionChecker {
private var host: String
private var port: UInt16
private var checkPermissionStatus: DispatchWorkItem?
private lazy var detectDeclineTimer: Timer? = Timer.scheduledTimer(
withTimeInterval: .zero,
repeats: false,
block: { [weak self] _ in
guard let checkPermissionStatus = self?.checkPermissionStatus else { return }
DispatchQueue.main.asyncAfter(deadline: .now(), execute: checkPermissionStatus)
})
init(host: String, port: UInt16, granted: #escaping () -> Void, failure: #escaping (Error?) -> Void) {
self.host = host
self.port = port
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationIsInBackground),
name: UIApplication.willResignActiveNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationIsInForeground),
name: UIApplication.didBecomeActiveNotification,
object: nil)
actionRequestNetworkPermissions(granted: granted, failure: failure)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/// Creating a network connection prompts the user for permission to access the local network. We do not have the need to actually send anything over the connection.
/// - Note: The user will only be prompted once for permission to access the local network. The first time they do this the app will be placed in the background while
/// the user is being prompted. We check for this to occur. If it does we invalidate our timer and allow the user to make a selection. When the app returns to the foreground
/// verify what they selected. If this is not the first time they are on this screen, the timer will not be invalidated and we will check the dispatchWorkItem block to see what
/// their selection was previously.
/// - Parameters:
/// - granted: Informs application that user has provided us with local network permission.
/// - failure: Something went awry.
private func actionRequestNetworkPermissions(granted: #escaping () -> Void, failure: #escaping (Error?) -> Void) {
guard let port = NWEndpoint.Port(rawValue: port) else { return }
let connection = NWConnection(host: NWEndpoint.Host(host), port: port, using: .udp)
connection.start(queue: .main)
checkPermissionStatus = DispatchWorkItem(block: { [weak self] in
if connection.state == .ready {
self?.detectDeclineTimer?.invalidate()
granted()
} else {
failure(nil)
}
})
detectDeclineTimer?.fireDate = Date() + 1
}
/// Permission prompt will throw the application in to the background and invalidate the timer.
#objc private func applicationIsInBackground() {
detectDeclineTimer?.invalidate()
}
/// - Important: DispatchWorkItem must be called after 1sec otherwise we are calling before the user state is updated.
#objc private func applicationIsInForeground() {
guard let checkPermissionStatus = checkPermissionStatus else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: checkPermissionStatus)
}
}
Declare outside the scope of the function in order to keep alive. Just remember to set to nil once you're done if the whole calling class isn't being deallocated later in order to unsubscribe to notifications.
Can be used like this:
class RandomClass {
var networkPermissionChecker: LocalNetworkPermissionChecker?
func checkPermissions() {
networkPermissionChecker = LocalNetworkPermissionChecker(host: "255.255.255.255", port: 4567,
granted: {
//Perform some action here...
},
failure: { error in
if let error = error {
print("Failed with error: \(error.localizedDescription)")
}
})
}
}

CallKit issue with call holding (iOS 10)

I have an issue with CallKit API on iOS 10.3.3 and lower iOS versions.
My configuration supports call holding and grouping, here is the snippet.
let configuration = CXProviderConfiguration(localizedName: applicationName)
configuration.supportsVideo = supportsVideo
configuration.supportedHandleTypes = [.phoneNumber]
configuration.maximumCallGroups = 2
configuration.maximumCallsPerCallGroup = 3
On iOS 10, CallKit drops the call when app requests setHeld action for call.
Here is the snippet of setHeld call action request.
public func performSetCallOnHoldAction(_ call: Call, isOnHold: Bool) {
let setHeldAction = CXSetHeldCallAction(call: UUID(uuidString: call.callUUID)!, onHold: isOnHold)
let transaction = CXTransaction()
transaction.addAction(setHeldAction)
requestTransaction(transaction)
}
And finally, here is the CXProvider delegate method, which completes the hold/unhold action for call:
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
// Retrieve the Call instance corresponding to the action's call UUID
guard let call = CallList.sharedInstance().call(withUUID: action.callUUID.uuidString) else {
action.fail()
return
}
// Perform PJSUA set held action
if action.isOnHold {
Pjsua2Wrapper.sharedInstance()?.holdCall(withCallUUID: call.callUUID, completion: { (error) in
(error != nil) ? action.fail() : action.fulfill()
})
} else {
Pjsua2Wrapper.sharedInstance()?.unholdCall(withCallUUID: call.callUUID, completion: { (error) in
(error != nil) ? action.fail() : action.fulfill()
})
}
}
Note that I use Pjsip to perform a hold/unhold action and inside a completion block I call action.fulfill() or action.fail(), depending wether psjip returns the error.
In application GUI, there is a toggle swap button, which calls function to perform CXSetHeldCallAction on CXCallController.
On iOS 11.0 everything works perfectly, but on iOS 10, when you press that toggle button, the call which is supposed to be set on hold, gets ended by the CXProvider for some reason (CXProvider responds with CXEndCallAction).
I have looked in debugger for requestTransaction method wether it returns transaction error, but there is no error in requesting a transaction to call controller.
Does anyone have an idea what is wrong, or something that I can try out to fix this issue?

iOS Audio not working during call answered when phone is locked. WebRTC used for calling

I am facing a problem with Audio When using Callkit with WebRTC for VOIP call, While answering the call from Lock Screen.
General Functionality :
My app activates the audioSession when it's launched. For an incoming call, SDP Offer & Answer are generated and exchanged. Peer Connection is set up. Both audio and video streams are generated, whether it's audio call or video call. Then Call is reported to callkit by using the following code:
callProvider.reportNewIncomingCall(with: currentCallUUID!, update: update) { error in }
If app is in the foreground, it works fine.
But, when the phone is locked, and user answers the call from lock screen, the Streams are exchanged but no audio comes on either end until user enters into the app himself.
As the user enters into the App, audio becomes active on both the ends.
All the background settings and capabilities are set properly.
I have also referred to the following work around provided by Apple staff. But even it does not work.
https://forums.developer.apple.com/thread/64544
As I mentioned, I am using WebRTC for calling. If I exchange the media streams after the user answers the call( still on Lock Screen) and peer connection is set at that time. It works fine (But it adds the delay in making the call connection).
But if Peer Connection is made before displaying call (say before reporting call to callkit), the audio stops working.
I am able to resolve this issue.
Steps that I followed -
I checked the code related to WebRTC here
I added RTCAudioSession header file which is actually a private class of Webrtc. So every time I receive a call event from signaling, I enable RTCAudiosession and on end of the call, I disable it.
I have to render the incoming streams to a dummy view (Although it is not displayed when the call is going and the app is not yet open, but it is required to make audio working).
I hope this will help if someone is facing the same issue.
#abhimanyu are you still facing the issue or you made it work. I am facing same issue with CallKit.
As per my understanding in WebRTC M60 release they have fixed on issue related to CallKit, which I think created a side effect and caused this issue.
The issue which they have fixed is related to System AudioSession, when ever CallKit presents incoming call UI and play ringer tone CallKit takes control of AudioSession and after user action (accept/ decline) it releases control. In WebRTC M60 release, now they have added observers for this control exchange. That's why it is working if app is in foreground, but if phone is locked and any incoming call is accepted then (I am assuming you are using CallKit UI for call and not redirecting user to App on accept from lock screen) due to Native UI of call it is not possible for WebRTC to activate its own AudioSession instance as call is going through CallKit Screen.
Link for bug which has been fixed on WebRTC M60: https://bugs.chromium.org/p/webrtc/issues/detail?id=7446
If you found any workaround for this issue please let me know.
Please Note that I share my code and its about to my needs and I share for reference. you need to change it according to your need.
when you receive voip notification create new incident of your webrtc handling class, and
add this two lines to code block because enabling audio session from voip notification fails
RTCAudioSession.sharedInstance().useManualAudio = true
RTCAudioSession.sharedInstance().isAudioEnabled = false
didReceive method;
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
let state = UIApplication.shared.applicationState
if(payload.dictionaryPayload["hangup"] == nil && state != .active
){
Globals.voipPayload = payload.dictionaryPayload as! [String:Any] // I pass parameters to Webrtc handler via Global singleton to create answer according to sdp sent by payload.
RTCAudioSession.sharedInstance().useManualAudio = true
RTCAudioSession.sharedInstance().isAudioEnabled = false
Globals.sipGateway = SipGateway() // my Webrtc and Janus gateway handler class
Globals.sipGateway?.configureCredentials(true) // I check janus gateway credentials stored in Shared preferences and initiate websocket connection and create peerconnection
to my janus gateway which is signaling server for my environment
initProvider() //Crating callkit provider
self.update.remoteHandle = CXHandle(type: .generic, value:String(describing: payload.dictionaryPayload["caller_id"]!))
Globals.callId = UUID()
let state = UIApplication.shared.applicationState
Globals.provider.reportNewIncomingCall(with:Globals.callId , update: self.update, completion: { error in
})
}
}
func initProvider(){
let config = CXProviderConfiguration(localizedName: "ulakBEL")
config.iconTemplateImageData = UIImage(named: "ulakbel")!.pngData()
config.ringtoneSound = "ringtone.caf"
// config.includesCallsInRecents = false;
config.supportsVideo = false
Globals.provider = CXProvider(configuration:config )
Globals.provider.setDelegate(self, queue: nil)
update = CXCallUpdate()
update.hasVideo = false
update.supportsDTMF = true
}
modify your didActivate and didDeActive delegate functions like below,
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
print("CallManager didActivate")
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = true
// self.callDelegate?.callIsAnswered()
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
print("CallManager didDeactivate")
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = false
}
in Webrtc handler class configure media senders and audiosession
private func createPeerConnection(webRTCCallbacks:PluginHandleWebRTCCallbacksDelegate) {
let rtcConfig = RTCConfiguration.init()
rtcConfig.iceServers = server.iceServers
rtcConfig.bundlePolicy = RTCBundlePolicy.maxBundle
rtcConfig.rtcpMuxPolicy = RTCRtcpMuxPolicy.require
rtcConfig.continualGatheringPolicy = .gatherContinually
rtcConfig.sdpSemantics = .planB
let constraints = RTCMediaConstraints(mandatoryConstraints: nil,
optionalConstraints: ["DtlsSrtpKeyAgreement":kRTCMediaConstraintsValueTrue])
pc = sessionFactory.peerConnection(with: rtcConfig, constraints: constraints, delegate: nil)
self.createMediaSenders()
self.configureAudioSession()
if webRTCCallbacks.getJsep() != nil{
handleRemoteJsep(webrtcCallbacks: webRTCCallbacks)
}
}
mediaSenders;
private func createMediaSenders() {
let streamId = "stream"
// Audio
let audioTrack = self.createAudioTrack()
self.pc.add(audioTrack, streamIds: [streamId])
// Video
/* let videoTrack = self.createVideoTrack()
self.localVideoTrack = videoTrack
self.peerConnection.add(videoTrack, streamIds: [streamId])
self.remoteVideoTrack = self.peerConnection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack
// Data
if let dataChannel = createDataChannel() {
dataChannel.delegate = self
self.localDataChannel = dataChannel
}*/
}
private func createAudioTrack() -> RTCAudioTrack {
let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
let audioSource = sessionFactory.audioSource(with: audioConstrains)
let audioTrack = sessionFactory.audioTrack(with: audioSource, trackId: "audio0")
return audioTrack
}
audioSession ;
private func configureAudioSession() {
self.rtcAudioSession.lockForConfiguration()
do {
try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue)
try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
} catch let error {
debugPrint("Error changeing AVAudioSession category: \(error)")
}
self.rtcAudioSession.unlockForConfiguration()
}
Please consider that because I worked with callbacks and delegates code includes delegates and callback chunks. you can ignore them accordingly!!
FOR REFERENCE You can also check the example at this link

How to keep native UI after accept a call using Callkit

I'm developing an iOS voip app using Callkit and Linphone. When I receive a incoming call, system shows the native phone UI to user accept or decline de call, whe the user taps accept button the call starts but de phone UI dissapear.
How can I keep native phone UI after user accept the call, like whatsapp do?
Also, how can I show the native phone UI when start a outgoing call?
Here's my providerDelegate code:
func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) {
// Construct a CXCallUpdate describing the incoming call, including the caller.
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: handle)
update.hasVideo = hasVideo
// Report the incoming call to the system
provider.reportNewIncomingCall(with: uuid, update: update) { error in
/*
Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error)
since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError.
*/
if error == nil {
print("calling")
}
}
}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let update = CXCallUpdate()
update.remoteHandle = action.handle
provider.reportOutgoingCall(with: action.uuid, startedConnectingAt: Date())
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "callStart"), object: self, userInfo: ["uuid":action.uuid])
action.fulfill(withDateStarted: Date())
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "callStart"), object: self, userInfo: ["uuid":action.uuid])
// ACCEPT CALL ON SIP MANAGER
if let voiceCallManager = AppDelegate.voiceCallManager {
voiceCallManager.acceptCall()
}
action.fulfill(withDateConnected: Date())
}
You can't keep native UI after accept the incoming call. And Whatsapp use their own UI, that is similar to native UI.
When you have iPhone locked and you accept an incoming call it won't show you APP UI. But if iPhone is unlocked and you accept an incoming call iPhone will open your app, and you must show your phone UI.
And for outgoing calls you can't show native phone UI, it will show if you receive an call.
Therefor, you need a custom phone UI for outgoing and established calls.

Resources