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

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?

Related

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 do I end the call session on callkit from my custom ongoing call UI?

When a user end a call from the CallKit UI the app ends the call and the actual VOIP call also end. But when I end the call from my custom UI the VOIP call ends but the CallKit is still active. How do I end the CallKit session from my custom UI?
This is what happens when I press end call on the CallKit UI:
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
XCPjsua.shared()?.endCall()
action.fulfill()
}
This is what happens when I end call from my custom UI (Should I close CallKit here?):
- (void)endcall {
[[XCPjsua sharedXCPjsua] endCall];
}
If you want to end the call from your custom UI you should do that through a CXTransaction:
let callController = CXCallController()
let endCallAction = CXEndCallAction(call: aUUID)
callController.request(
CXTransaction(action: endCallAction),
completion: { error in
if let error = error {
print("Error: \(error)")
} else {
print("Success")
}
})
this will cause provider(_ provider: CXProvider, perform action: CXEndCallAction) to be called.
In all other cases (i.e. remote ended, unanswered, etc... - see CXCallEndedReason) you should only report the ended call:
let provider: CXProvider
provider.reportCall(with: call.uuid, endedAt: Date(), reason: .remoteEnded)
in this case provider(_ provider: CXProvider, perform action: CXEndCallAction) will not be called.
I use this code to close call
provider?.reportCall(with: PushUtility.shared.uuid!, endedAt: Date(), reason: .remoteEnded)
First save your uuid which you are using for connecting call use that uuid for end call.
I managed to close it using the reportCall function
provider?.reportCall(with: appG.uuid, endedAt: Date(), reason: .remoteEnded)
So I just call that function when I press end call from my custom UI

Calling screen dismiss on accept the twilio voice call even app is active in swift 4

I’m using Twilio’s Programmable Voice in one of the projects. My primary requirement is to place VoIP class between mobile devices. I am able to place calls from one device to another,but when i accept the call at that time calling screen is dismiss automatically and call continue in background. In this case user do not have an option for disconnect call or any other action related to call because screen is dismissed.
Here is the screen that i have created for call when app is in foreground.
Calling placed success fully but on receiver accept it will dismiss the custom screen.So that user do not have any option to disconnect call or any other action related to call.
If any issue in code or any thing related to call kit setting i need to configure or any other issue ? Please help.
As per my knowledge this is default behaviour of call kit framework. On accept button click it will dismiss the screen when app is in foreground. If you wants to achieve same like whats app then you need to create a custom screen for that.Below code I did to resolve this issue:
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction)
{
NSLog("provider:performAnswerCallAction:")
// TwilioVoice.configureAudioSession()
let vc = loadVC(strStoryboardId: SB_CHAT, strVCId: idVoiceCallVC) as! VoiceCallVC
vc.callername = name
vc.userPhoto = userphoto
APP_DELEGATE.appNavigation?.pushViewController(vc, animated: true)
assert(action.callUUID == self.callInvite?.uuid)
TwilioVoice.isAudioEnabled = false
self.performAnswerVoiceCall(uuid: action.callUUID)
{ (success) in
if (success)
{
action.fulfill()
}
else
{
action.fail()
}
}
action.fulfill()
}
You just need to add your custom screen display code in this delegate method of call kit framework.
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {}
Thanks.

Group calls in Callkit

I've developed a VoIP app. I'm integrating now with callkit framework. Everything works well except conference.
The situation is the following:
1.) I make a call.
2.) Put first call on hold and make another.
3.) I click Conference button to merge both calls.
If I manually remove hold for first call, automatically second call is on hold.
I've read about CXSetGroupCallAction, but there is no match documentation.
Can someone help me?
Thanks.
Call perform merge call action
let call1UUID = UUID(uuidString: call1.callUUID)!
let call2UUID = UUID(uuidString: call2.callUUID)!
let mergeCallAction = CXSetGroupCallAction(call: call1UUID, callUUIDToGroupWith: call2UUID)
let transaction = CXTransaction()
transaction.addAction(mergeCallAction)
callController.request(transaction) { (_) in
}
This will call provider delegate:
func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
// perform merge call here where you merge ports of two call audio i/o
action.fulfill()
}

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