How to keep native UI after accept a call using Callkit - ios

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.

Related

How to set timeout for CallKit incoming call UI

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.

CallKit: Resume an on hold VoIP call after GSM call disconnects

I'm experiencing an issue where I'm unable to resume an on hold VoIP call after a GSM call is disconnected by the calling person.
Scenario one that works fine:
I answer an incoming VoIP call
I get a GSM call, press 'hold and accept', which results in provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) being called, allowing me to put the VoIP call on hold
I hang up the GSM call which results in provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) being called, allowing me to un-hold the VoIP call
Scenario two:
I answer an incoming VoIP call
I get a GSM call, press 'hold and accept', which results in provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) being called, allowing me to put the VoIP call on hold
The caller hangs up the GSM call, but provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) is not called
In scenario two only provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) is called after the caller hangs up the GSM call.
How can we resume a VoIP call after a GSM is disconnected by the caller if provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) is not called?
public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction)
{
print("Callkit: provider(_ provider: CXProvider, perform action: CXSetHeldCallAction): \(action.callUUID)")
guard var call = callManager.call(withUUID: action.callUUID) else {
action.fail()
return
}
call.isOnHold = action.isOnHold
call.isMuted = call.isOnHold
audioRouter.audioDevice.isEnabled = !call.isOnHold
action.fulfill()
}
After A GSM call is ended, applicationDidBecomeActive will be called when returning to the app. At this point we can detect if a call is on hold and resume it. Other solutions welcome.
I ran into this today. After some Googling, I was unable to find a solution. So strange, I thought, that nobody has solved this. I was able to find a solution inspired by #Simon, but I think a bit cleaner.
In your CallViewController, AppDelegate, or similar, set yourself as the CXCallObserver's delegate.
CXCallController.shared.callObserver.setDelegate(self, queue: .main)
Then later, you want to detect if there is exactly one active call, it's "yours" (whatever that means for you. For me it's stored in self.call.id) and that it's on hold. If so, you can request an update to "unhold" it, and your code elsewhere that handles func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) will get invoked, and hopefully your app handles it correctly.
//MARK: - CXCallObserverDelegate
func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) {
guard let activeCall = callObserver.calls.first,
1 == callObserver.calls.count,
activeCall.uuid == self.call.id,
true == activeCall.isOnHold else {
return
}
let transaction = CXTransaction(action: CXSetHeldCallAction(call: activeCall.uuid, onHold: false))
CXCallController.shared.request(transaction) { error in
if let error = error {
//handle it?
}
}
}
When you get an incoming call, from say, WhatsApp, and your app is put on hold, CXCallObserver.shared.calls will return two calls. One will be "yours" on hold, and the other will be "the WhatsApp call" not on hold. Once the WhatsApp call is terminated on the other end, CXCallObserver.shared.calls will only return one call, yours, but it will still be on hold. This seems like a fairly egregious bug in CallKit. But what do I know?

How to set up the Twilio iOS SDK to connect to the server and make phone call?

I am new to iOS development, and trying to use Twilio to deliver a voice conference application. I followed the documentation to setup the example from https://github.com/twilio/video-quickstart-ios, and I successfully made a few phone calls to the Twilio Bin.
However, when I try to migrate it to my own application, it somehow doesn't work. I copied the Server folder and setup the .env accordingly, and dialed a call. I saw my phone enter the status of calling, but it doesn't connect to any phone call. I checked my source code and noticed that it was the callDidConnect function in CallDelegate wasn't triggered. Therefore my phone hung on the "dialing" but never connected to any phone call. And I didn't see any network activity anyhow.
Here are my questions:
Do I need the Server folder to actually dial a phone call?
How does iOS client connect to any server, either Twilio or my own Nodejs server? I dont' see a line that is doing it.
Where do I put my API_SID_KEY, AUTH_TOKEN ...etc if I want to build my own server?
Could anyone provide an example repository of using Twilio to make phone call?
I already had some knowledge in CallKit, how is CallKit integrated
I am attaching my source code below, they are simply prototype, I have simplified to help me to figure out how it works:
When user clicks the dial button, it calls this function:
func performVoiceCall(uuid: UUID, client: String?, completionHandler: #escaping (Bool) -> Void) {
let connectOptions = ConnectOptions(accessToken: accessToken) { builder in
builder.params = [self.twimlParamTo: ""]
builder.uuid = uuid
}
let call = TwilioVoiceSDK.connect(options: connectOptions, delegate: self)
callKitCompletionCallback = completionHandler
}
then this function will be triggered by above function and with success
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: Date())
performVoiceCall(uuid: action.callUUID, client: "") { success in
if success {
NSLog("performVoiceCall() successfully")
provider.reportOutgoingCall(with: action.callUUID, connectedAt: Date())
} else {
NSLog("performVoiceCall() failed")
}
}
action.fulfill()
}
Eventually I expected the following functions will be triggered, BUT IT DOES NOT:
extension RoomPageViewModel: CallDelegate {
func callDidConnect(call: Call) {
print("Call did connect")
}
func callDidFailToConnect(call: Call, error: Error) {
print("Call did fail to connect", error.localizedDescription)
}
func callDidDisconnect(call: Call, error: Error?) {
print("Call did disconnect")
}
}
I believe the biggest problem is that I don't understand how the client actually communicates with either Twilio or my NodeJS, therefore I don't know why the callDidConnect function is not triggered

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

Resources