CallKit Audio session starts when navigating to the application only - ios

I'm working on a voip app now and want to support holding.
But when a second call comes and I hold my current call. Switching to my first call I hear no sound at all.
The way to hear it is to navigate from callKit native screen to my app and hence I can hear the voice.
func configureAudioSession() {
_ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, mode: .videoChat, options: AVAudioSession.CategoryOptions.mixWithOthers)
_ = try? AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none)
_ = try? AVAudioSession.sharedInstance().setMode(AVAudioSession.Mode.voiceChat)
}
func startAudio() {
print("Starting audio")
do {
_ = try AVAudioSession.sharedInstance().setActive(true)
} catch {
}
}
func stopAudio() {
print("Stopping audio")
do {
_ = try AVAudioSession.sharedInstance().setActive(false)
} catch {
}
}

For supporting holding, you dont have to start/stop audio session, instead you can use CXSetHeldCallAction provided by Callkit itself. Here, is the code for hold that i use.
let callKitCallController = CXCallController()
func performHoldAction(isOnHold:Bool, uuid:UUID) {
let holdCallAction = CXSetHeldCallAction(call: uuid, onHold: isOnHold)
let transaction = CXTransaction(action: holdCallAction)
callKitCallController.request(transaction) { error in
if let error = error {
CPrint("holdCallAction transaction request failed: \(error.localizedDescription).")
return
}
CPrint("holdCallAction transaction request successful")
}
}
Once system puts the call on hold(by above method OR due to other incoming call accepting or any other reason), then in CXProviderDelegate, the method func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) gives you the callback for the detail.
Here, system/callkit itself interacts with audio, you dont have to explicitly start or stop audio for holding.
Note: Make sure that you given supportsHolding to true for the CXCallUpdate that you gave for new call.

Related

Apple Siri Intent stops playback without apparent reason

I would like to improve the integration of my App FM-Pod with Siri intents shortcuts. I've done this App to listen the radio on the HomePod and, as of now, I've been able to start the playback, change the stations, etc. but I'm facing a strange issue which causes the audio playback to stop alone after about 1 minute...
Does anyone knows the reason? What's wrong?
Here is the code in Swift for starting the playback, leveraging on AVAudioPlayer:
open func handle(intent: StartFMPodIntent, completion: #escaping (StartFMPodIntentResponse) -> Void) {
DataManager.getStationDataWithSuccess(filename: "favorites") { (data) in
if debug { print("Stations JSON Found") }
guard let data = data, let jsonDictionary = try? JSONDecoder().decode([String: [RadioStation]].self, from: data), let stationsArray = jsonDictionary["station"]
else {
if debug { print("JSON Station Loading Error") }
return
}
HPRIntentHandler.stations = stationsArray
if !FRadioPlayer.shared.isPlaying {
FRadioPlayer.shared.radioURL = URL(string: HPRIntentHandler.stations![0].streamURL0!)
let response = StartFMPodIntentResponse(code: .success, userActivity: nil)
response.stationName = stationsArray[0].name
completion(response)
}
}
}
According to https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionOverview.html#//apple_ref/doc/uid/TP40014214-CH2-SW2
Extension is killed after code is finished running.
Since the getStationDataWithSuccess run in Asynchronously, its code reaches the end before the asynchronous function finishes running.
You have to find different place to handle this.

duckOthers Audio Session Interrupted by Remote Push Notification

I have a workout app that plays short clips of sound every couple of seconds. I have background music enabled so that music from other apps can be played while working out. The problem arises when I get a remote push notification (in my case, Slack) that has a sound, which somehow cancels out my duckingOther audio session and the music from other apps becomes loud again.
Question - How do I reset my duckingOthers audiosession when the user gets this type of interruption?
I set the audio session by calling the below function in didFinishLaunchingWithOptions:
private func setupAudioSession(){
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: [.mixWithOthers, .duckOthers, .interruptSpokenAudioAndMixWithOthers])
print("AVAudioSession Category Playback OK")
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
I have tried treating this as a hard interruption (for example a phone call), but when trying to apply the techniques used for this type of interruption, it seems that remote push notifications pass through the cracks. Below is what I used from a different question to try and catch interruptions.
#objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions
print("interruption started")
}
else if type == .ended {
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
print("should resume playback")
setupAudioSession()
} else {
// Interruption Ended - playback should NOT resume
print("should not resume playback")
}
}
}
}
func setupNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: nil)
}

How to remove callback option UI of CallKit after ending the call

In my app i'm using CallKit for incoming call. There is no outgoing call feature in the app. Everything is fine but when the receiver or dailer ends the call it shows the CallKit UI with call back option. I don't want to show callback option, how can I do it?
My code for ending the call
func end(call: SpeakerboxCall) {
let endCallAction = CXEndCallAction(call: call.uuid)
let transaction = CXTransaction()
transaction.addAction(endCallAction)
requestTransaction(transaction, action: "endCall")
}
private func requestTransaction(_ transaction: CXTransaction, action:
String = "") {
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction \(action) successfully")
}
}
}
I have solved it. I was force quitting the CallKit where the transaction is not correctly completing.
AppDelegate.shared.providerDelegate?.provider.reportCall(with: call.uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded)
We need to set endedAt to nil and reason to remoteEnded
Close All Call (Swift4)
func performEndCallAction() {
for call in self.cxCallController.callObserver.calls {
let endCallAction = CXEndCallAction(call: call.uuid)
let transaction = CXTransaction(action: endCallAction)
cxCallController.request(transaction) { error in
if let error = error {
NSLog("EndCallAction transaction request failed: \(error.localizedDescription).")
return
}
NSLog("EndCallAction transaction request successful")
}
}
}

Dispatching delegate callback to main queue, from another queue, AVFoundation

I am working on a camera that is wrapped up in a base viewController, with a delegate style interface for callbacks, so all I have to do as a client is subclass the camera view controller, implement the delegate methods, and add the UI buttons.
My question is about recording video. Video recording is started on a unique background task to ensure that the recording can be written to a temporary file. This is done on my
private let sessionQueue = DispatchQueue(label: "com.andrewferrarone.sessionQueue") session queue:
public func startRecording()
{
guard let movieFileOutput = self.movieFileOutput else { return }
//get video preview layer's video orientation on main queue
guard let videoPreviewLayerOrientation = self.previewView.videoPreviewLayer?.connection.videoOrientation else {
print("handleRecord: videoPreviewLayer is nil")
return
}
self.sessionQueue.async {
if !movieFileOutput.isRecording {
if UIDevice.current.isMultitaskingSupported {
self.backgroundRecordingID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
}
//update orientation on the movie file output video connection before recording
let movieFileOutputConnection = self.movieFileOutput?.connection(withMediaType: AVMediaTypeVideo)
movieFileOutputConnection?.videoOrientation = videoPreviewLayerOrientation
//start recording to a temporary file:
let outputFileName = UUID().uuidString
let outputFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((outputFileName as NSString).appendingPathExtension("mov")!)
movieFileOutput.startRecording(toOutputFileURL: URL(fileURLWithPath: outputFilePath), recordingDelegate: self)
}
}
}
so the recording is setup as a background task dispatched to self.sessionQueue. When I stop recording I receive an AVCaptureFileOutputRecordingDelegate method. In this method, I want to callback my delegate with the filepath, and then cleanup. How do I ensure that the delegate can persist the recording from the temporary file path before cleanup happens and the temporary file is removed?
public func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!)
{
//cleanup func for later
func cleanup()
{
let path = outputFileURL.path
if FileManager.default.fileExists(atPath: path) {
do {
try FileManager.default.removeItem(atPath: path)
}
catch let error {
print("Could not remove file at url: \(outputFileURL), error: \(error.localizedDescription)")
}
}
if let currentBackgroundRecordingID = self.backgroundRecordingID {
self.backgroundRecordingID = UIBackgroundTaskInvalid
if currentBackgroundRecordingID != UIBackgroundTaskInvalid {
UIApplication.shared.endBackgroundTask(currentBackgroundRecordingID)
}
}
}
var success = true
if error != nil {
print("Movie file finishing error: \(error)")
success = ((error as NSError).userInfo[AVErrorRecordingSuccessfullyFinishedKey] as AnyObject).boolValue
}
DispatchQueue.main.async {
self.delegate?.camera(self, didFinishRecordingToOutputFileAt: outputFileURL, success: success)
}
cleanup()
}
So I called my delegate back on the main queue with the results, but then I need to call cleanup() should I do this on the main queue right after calling back my delegate? is this safe? or if I leave it the way it is now, then we are on self.sessionQueue, and I am unsure if cleanup() will happen before the delegate method implementation has time to persist the recording. If anyone can give me some insight into what is going on and what would be the safest thing to do here, that would be great. According to apple, the documentation says do not assume that AVCaptureFileOutputRecordingDelegate didFinishRecordingToOutputFileAt method is called on a specific thread. Thanks for your time and help!
How about:
DispatchQueue.main.async {
self.delegate?.camera(self, didFinishRecordingToOutputFileAt: outputFileURL, success: success)
self.sessionQueue.async {
cleanup()
}
}
I think that would be the standard way of handling this situation. When the delegate method finishes, you assume that the delegate is done with the file (or copied it somewhere else).

How can I simulate an Outgoing call using iOS Callkit

I have gone through the iOS CallKit documentation, the below is the code provided by Apple under the section 'Making Outgoing Calls'. When I try and call the function startOutGoingCall() nothing happens i.e. I don't see any outgoing call UI.
Could someone please suggest me how I could trigger a native outgoing call UI.
func startOutGoingCall(){
let uuid = UUID()
let handle = CXHandle(type: .emailAddress, value: "jappleseed#apple.com")
let startCallAction = CXStartCallAction(call: uuid)
startCallAction.destination = handle
let transaction = CXTransaction(action: startCallAction)
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction successfully")
}
}
}
EDIT:
Added the delegate method from my code
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
logMessage(messageText: "provider:performStartCallAction:")
/*
* Configure the audio session, but do not start call audio here, since it must be done once
* the audio session has been activated by the system after having its priority elevated.
*/
localMedia?.audioController.configureAudioSession(.videoChatSpeaker)
callKitProvider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
performRoomConnect(uuid: action.callUUID, roomName: action.handle.value) { (success) in
if (success) {
provider.reportOutgoingCall(with: action.callUUID, connectedAt: Date())
action.fulfill()
} else {
action.fail()
}
}
}
There is no outgoing UI you get from CallKit. When you make an outgoing call, your app is open, and therefore, it is your app that should show the UI.

Resources