Audio playback lock screen control not displaying - swift

I am trying display audio controls on the lock screen, but the problem is the audio control does not display anything in the lock screen. I already enabled background modes and the audio plays in the background.
In app delegate class, right when my app launches, I set up my audio session
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
setupAudioSession()
UIApplication.shared.beginReceivingRemoteControlEvents()
return true
}
func setupAudioSession(){
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: [])
self.becomeFirstResponder()
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)
}
}
In my main controller, I call the setupLockScreen function after I play the audio.
func setupLockScreen(){
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
if self.player?.rate == 0.0 {
self.player?.play()
return .success
}
return .commandFailed
}
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "My Song"
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = audioplayerItem.duration.seconds
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = audioplayerItem.asset.duration.seconds
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player?.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
I read many articles and I looked all the qustions in Stack Overflow but no luck.

Is the problem that you nowPlayingInfo is not displaying on the lock screen or is it that the controls you added are not responding?
To have your nowPlayingInfo appear on the lockscreen, you need to do a few things listed here in an answer on Handling External Player Events Notifications. One is to have a non mixable audio session (which you already have with AVAudioSessionCategoryPlayback) and the other is to be playing audio or only recently have stopped playing audio. Is your app actually playing audio?
Background audio, which you have is a sort of derived requirement, because lockscreen=>background=>background audio needed to play audio, so I should add that to the other answer.
If the problem is that the lockscreen controls aren't enabled/responsive, then try adding a pauseCommand as well. The playCommand/pauseCommand pair seem to be a minimum requirement for receiving lockscreen/external player commands.
p.s. your MPNowPlayingInfoPropertyElapsedPlaybackTime looks wrong. shouldn't it be
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(audioplayerItem.currentTime)

Do it like this (this is swift 3):
override func viewDidLoad() {
super.viewDidLoad()
UIApplication.shared.beginReceivingRemoteControlEvents()
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.pauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
//Update your button here for the pause command
return .success
}
commandCenter.playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
//Update your button here for the play command
return .success
}
}
This answer found HERE

Related

I want to do a download that can continue in the background in swift, but when I switch to the background, the bluetooth file transfer stops

func applicationDidEnterBackground(_ application: UIApplication) {
print("Entered Background")
bgTask = application.beginBackgroundTask(expirationHandler: {
self.bgTask = UIBackgroundTaskIdentifier.invalid
self.cb_central = CBCentralManager()
self.cb_central.delegate = self
self.centralManagerDidUpdateState(self.cb_central)
Definations.isFileTransferInProgress = true
VoiceRecordMainVC().downloadFile()
})
}
func applicationWillEnterForeground(_ application: UIApplication) {
UIApplication.shared.endBackgroundTask(bgTask)
}
I have such a code and when it goes to the background, I tell the download to continue, actually I am telling it to start the download again. Do you think it is possible to do this and how can I do it, thank you for your help.

MPRemoteCommandCenter not available after changing audio session category

My app has got the option to allow its sound to be mixed with other apps.
According to Apple, MPRemoteCommandCenter is only available when apps do not allow for mixing.
Inside my app, when the user taps the button to change the mixWithOthers setting, the audio session is set accordingly.
However, even when the user switches back to not allow mixing anymore MPRemoteCommandCenter will not show up in lock screen until the app has been removed from cache (swiped up) and started again.
Is there a way to achieve the desired behaviour without having to re-start the app?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UIApplication.shared.beginReceivingRemoteControlEvents()
}
var isMixWithOthersAllowed: Bool
func startProcess() {
setAudioSession {
setupMediaControls()
}
}
func setAudioSession(completion: #escaping () -> Void) {
let audioSession = AVAudioSession.sharedInstance()
do {
if isMixWithOthersAllowed {
try audioSession.setCategory(.playback, options: [.mixWithOthers])
/* The remote command center will not be available when mixing with others,
as stated by Apple in the docs. */
} else {
try audioSession.setCategory(.playback)
/* The remote command center should be available when switching back to
this category, but it will only show up after the app has been killed
and started fresh again. I'd like it to be available without restarting
the application. */
}
try audioSession.setActive(true)
} catch let error as NSError {
configureAudioSessionError = error
}
assert(configureAudioSessionError == nil, "Create audio session failed")
completion()
}
func setupMediaControls() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.isEnabled = true
// Setup handlers for commands
// ...
setupNowPlaying()
}
func setupNowPlaying() {
// Configure audio track metadata and update UI elements
}

open to deep link from local notification when app is inactive

I want to schedule local notification for X minutes and take user to a specified link when actioned.
currently when the app is in foreground or inactive the delegate method UNUserNotificationCenter(didReceive: withCompletionHandler:) is called and the app works as expected (the deep link opens)
the issue I'm running into is when the notification is received when the app is suspended or background and the notification launches the application I cannot seem to capture where the link is received and cannot follow the link, from what I can see that delegate method is not called?
below is the implementation for UNUserNotificationCenter(didReceive: withCompletionHandler:)
defer {
completionHandler()
}
guard
response.actionIdentifier == UNNotificationDefaultActionIdentifier ||
response.actionIdentifier == "open-dl" else {
return
}
guard
let url = response.notification.request.content.userInfo["link-to"] as? String,
let linkTo = URL(string: url) else {
return
}
if UIApplication.shared.applicationState == .background {
UserDefaults.standard.set(linkTo, forKey: "localdeeplink")
UserDefaults.standard.synchronize()
} else {
_ = UIApplication.shared.delegate?.application?(UIApplication.shared, open: linkTo, options: [:])
}
When I attempt to read that localdeeplink entry back out of UserDefaults it's empty.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//add this function
if launchOptions != nil {
pushAction(launchOptions: launchOptions)
}
return true
}
func pushAction(launchOptions: [NSObject: AnyObject]?) {
//add code of UNUserNotificationCenter(didReceive: withCompletionHandler:) here
}
Add the code of UNUserNotificationCenter(didReceive: withCompletionHandler:) in pushAction function
When app is closed didRecive is not getting called, didFinish is called with the push payload.
Apple link go to Handling Remote Notifications at bottom

iBeacon Ranging in background

I'm trying to set my application as a way to just start ringing in the background and get a notification when the user hit the shoulder button or home button(not all the time) and I don't want using background mode. So I coded this in swift, it works when my app is working in foreground and background for 10 seconds and ranging won't be restarted when the user illuminates his phone's screen. First I started background task and then in app delegate I was trying to start ranging for the beacon in the immediate range. Can someone help me about this?
#Background Task
class BackgroundTask1 : NSObject {
private let application: UIApplication!
private var identifier = UIBackgroundTaskInvalid
init(application: UIApplication) {
self.application = application
}
class func run(application: UIApplication, handler: (BackgroundTask1) -> ()) {
let backgroundTask = BackgroundTask1(application: application)
backgroundTask.begin()
handler(backgroundTask)
}
func begin() {
print("begin")
self.identifier = application.beginBackgroundTask {
self.end()
}
}
func end() {
if (identifier != UIBackgroundTaskInvalid) {
application.endBackgroundTask(identifier)
}
identifier = UIBackgroundTaskInvalid
}}
#AppDelegate
func init_()
{
let uuidString = "43F2ACD1-5522-4E0D-9E3F-4A828EA12C24"
let beaconRegionIdentifier = "Hello"
let beaconUUID:UUID = UUID(uuidString:uuidString)!
beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID, identifier: beaconRegionIdentifier)
beaconRegion.notifyEntryStateOnDisplay = true
locationManager = CLLocationManager()
if (CLLocationManager.authorizationStatus() != CLAuthorizationStatus.authorizedWhenInUse) {
locationManager!.requestWhenInUseAuthorization()
}
locationManager!.delegate = self
locationManager!.pausesLocationUpdatesAutomatically=false
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
init_()
locationManager?.startRangingBeacons(in: beaconRegion)
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
var timer = Timer()
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
BackgroundTask1.run(application:
application) { (BackgroundTask1_) in
DispatchQueue.global(qos: .default).async
{
DispatchQueue.main.async {
self.init_()
self.locationManager?.startRangingBeacons(in: self.beaconRegion)
}
}
}
}

How to resume audio after interruption in Swift?

I am following instructions here, I've put together this test project to handle interruptions to audio play. Specifically, I'm using the alarm from the default iphone clock app as interruption. It appears that the interruption handler is getting called but is not getting past the let = interruptionType line as "wrong type" showed up twice.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player = AVAudioPlayer()
let audioPath = NSBundle.mainBundle().pathForResource("rachmaninov-romance-sixhands-alianello", ofType: "mp3")!
func handleInterruption(notification: NSNotification) {
guard let interruptionType = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? AVAudioSessionInterruptionType else { print("wrong type"); return }
switch interruptionType {
case .Began:
print("began")
// player is paused and session is inactive. need to update UI)
player.pause()
print("audio paused")
default:
print("ended")
/**/
if let option = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? AVAudioSessionInterruptionOptions where option == .ShouldResume {
// ok to resume playing, re activate session and resume playing
// need to update UI
player.play()
print("audio resumed")
}
/**/
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
do {
try player = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: audioPath))
player.numberOfLoops = -1 // play indefinitely
player.prepareToPlay()
//player.delegate = player
} catch {
// process error here
}
// enable play in background https://stackoverflow.com/a/30280699/1827488 but this audio still gets interrupted by alerts
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
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)
}
// add observer to handle audio interruptions
// using 'object: nil' does not have a noticeable effect
let theSession = AVAudioSession.sharedInstance()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.handleInterruption(_:)), name: AVAudioSessionInterruptionNotification, object: theSession)
// start playing audio
player.play()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Furthermore, following an idea here, I have modified the handler to
func handleInterruption(notification: NSNotification) {
//guard let interruptionType = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? AVAudioSessionInterruptionType else { print("wrong type"); return }
if notification.name != AVAudioSessionInterruptionNotification
|| notification.userInfo == nil{
return
}
var info = notification.userInfo!
var intValue: UInt = 0
(info[AVAudioSessionInterruptionTypeKey] as! NSValue).getValue(&intValue)
if let interruptionType = AVAudioSessionInterruptionType(rawValue: intValue) {
switch interruptionType {
case .Began:
print("began")
// player is paused and session is inactive. need to update UI)
player.pause()
print("audio paused")
default:
print("ended")
/** /
if let option = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? AVAudioSessionInterruptionOptions where option == .ShouldResume {
// ok to resume playing, re activate session and resume playing
// need to update UI
player.play()
print("audio resumed")
}
/ **/
player.play()
print("audio resumed")
}
}
}
Results are that all of "began", "audio paused", "ended" and "audio resumed" show up in console but audio play is not actually resumed.
Note: I moved the player.play() outside of the commented out where option == .ShouldResume if statement because that if condition is not true when the .Ended interruption occurs.
(Posted on behalf of the question author, after it was posted in the question).
Solution found! Following discussion here, inserted this in viewDidLoad()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: AVAudioSessionCategoryOptions.MixWithOthers)
} catch {
}
After clicking "ok" on the alarm interruption, the audio play continued. Unlike previously noted, the solution does NOT require an interruption handler (which #Leo Dabus has since removed).
However if you are using an interruption handler, .play() must NOT be invoked within handleInterruption() as doing so does NOT guarantee play to resume & seems to prevent audioPlayerEndInterruption() to be called (see docs). Instead .play() must be invoked within audioPlayerEndInterruption() (any of its 3 versions) to guarantee resumption.
Furthermore, AVAudioSession must be give option .MixWithOthers noted by #Simon Newstead if you want your app to resume play after interruption when your app is in the background. It seems that if a user wants the app to continue playing when it goes into the background, it is logical to assume the user also wants the app to resume playing after an interruption while the app is in the background. Indeed that is the behaviour exhibited by the Apple Music app.
#rockhammers suggestion worked for me. Here
before class
let theSession = AVAudioSession.sharedInstance()
in viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleInterruption(notification:)), name: NSNotification.Name.AVAudioSessionInterruption, object: theSession)
And then the Function
func handleInterruption(notification: NSNotification) {
print("handleInterruption")
guard let value = (notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? NSNumber)?.uintValue,
let interruptionType = AVAudioSessionInterruptionType(rawValue: value)
else {
print("notification.userInfo?[AVAudioSessionInterruptionTypeKey]", notification.userInfo?[AVAudioSessionInterruptionTypeKey])
return }
switch interruptionType {
case .began:
print("began")
vox.pause()
music.pause()
print("audioPlayer.playing", vox.isPlaying)
/**/
do {
try theSession.setActive(false)
print("AVAudioSession is inactive")
} catch let error as NSError {
print(error.localizedDescription)
}
pause()
default :
print("ended")
if let optionValue = (notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? NSNumber)?.uintValue, AVAudioSessionInterruptionOptions(rawValue: optionValue) == .shouldResume {
print("should resume")
// ok to resume playing, re activate session and resume playing
/**/
do {
try theSession.setActive(true)
print("AVAudioSession is Active again")
vox.play()
music.play()
} catch let error as NSError {
print(error.localizedDescription)
}
play()
}
}
}
some reasons interruptionNotification is not working correctly on iOS 12.x So I added silenceSecondaryAudioHintNotification
With alarm notification incoming, you can try to use silenceSecondaryAudioHintNotification.
#objc func handleSecondaryAudioSilence(notification: NSNotification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt,
let type = AVAudioSession.SilenceSecondaryAudioHintType(rawValue: typeValue) else {
return
}
if type == .end {
// Other app audio stopped playing - restart secondary audio.
reconnectAVPlayer()
}
}