iOS Swift - MPCommandCenter & NowPlayingInfoCenter Controlling Music on Lock Screen - ios

I am creating a music player (In Swift 3) that uses MPMediaItems and MPMediaPlayerController. I cannot for the life of me figure out how to control music from the lock screen or notification center...
I have read every article I can find on MPRemoteCommandCenter and MPNowPlayingInfoCenter and I cannot get it to work.
I have enabled background music playback, currently the music continues playing outside of the app, but does not received remote commands.
Below is the code currently being used
In my View Did Load I call the following function
let player = MPMusicPlayerController.applicationMusicPlayer()
let commandCenter = MPRemoteCommandCenter.shared()
func configureCommandCenter() {
print("Enter configuration")
self.commandCenter.playCommand.addTarget { [weak self] event -> MPRemoteCommandHandlerStatus in
guard let sself = self else { return .commandFailed }
print("Play")
sself.player.play()
self?.getNowPlayingItem()
return .success
}
self.commandCenter.pauseCommand.addTarget { [weak self] event -> MPRemoteCommandHandlerStatus in
guard let sself = self else { return .commandFailed }
print("Pause")
sself.player.pause()
self?.getNowPlayingItem()
return .success
}
self.commandCenter.nextTrackCommand.addTarget { [weak self] event -> MPRemoteCommandHandlerStatus in
guard let sself = self else { return .commandFailed }
print("next")
sself.player.skipToNextItem()
self?.getNowPlayingItem()
return .success
}
self.commandCenter.previousTrackCommand.addTarget { [weak self] event -> MPRemoteCommandHandlerStatus in
guard let sself = self else { return .commandFailed }
print("Prev")
sself.player.skipToPreviousItem()
self?.getNowPlayingItem()
return .success
}
}
To reiterate my project compiles fine, plays media, continues playing media when app is not in focus and when phone is locked, however no commands are seen from within the app, resulting in the app not being able to be controlled from the lock screen or notification center. Any help would be greatly appreciated.
I would also like to mention that I have looked at the Apple API Docs related to both RemoteCommands and InfoCenter.
Am I missing some key step in order to get remote commands registering from within the app?

The problem is that your player is MPMusicPlayerController.applicationMusicPlayer(). You cannot use the application music player as a remote control target.
If you want remote control target capabilities, you need your player to be something like an AVAudioPlayer.

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.

setup MPRemoteCommandCenter in watchOS Now Playing with WKAudioFilePlayer

I'm working on an app that play audio in Apple Watch; All working fine from inside the app.
I'm trying to setup the MPRemoteCommandCenter in 'Now Playing' to change the next/prev track to skipForward/skipBackward and to add a handler for the pause command.
Nothing change in Now Playing commands the the handler not being trigged.
Below a snippet code:
Play method
var player: WKAudioFilePlayer!
#IBAction func play() {
let avSession = AVAudioSession.sharedInstance()
try! avSession.setCategory(.playback, mode: .default, policy: .longForm, options: [])
let url = Bundle.main.url(forResource: "sample", withExtension: "mp3")!
let item = WKAudioFilePlayerItem(asset: WKAudioFileAsset(url: url))
player = WKAudioFilePlayer(playerItem: item)
do {
try avSession.setActive(true, options: .notifyOthersOnDeactivation)
player.play()
} catch {
print("Error")
}
}
Remote controls setup method:
func setupRemoteControls() {
// Get the shared MPRemoteCommandCenter
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: 15)]
commandCenter.skipForwardCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
// skip forward
return .success
}
commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: 15)]
commandCenter.skipBackwardCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
// skip backword
return .success
}
commandCenter.skipBackwardCommand.isEnabled = true
// Add handler for play Command
commandCenter.playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
self.player.play()
return .success
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
self.player.pause()
return .success
}
}
I'm calling self.setupRemoteControls() in awake method. Also I tried to move the setup to the ExtensionDelegate -> applicationDidFinishLaunching
Apple references I used:
https://developer.apple.com/videos/play/wwdc2018/504/
https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/MediaPlaybackGuide/Contents/Resources/en.lproj/RefiningTheUserExperience/RefiningTheUserExperience.html
:: UPDATE ::
I found when using the AVAudioPlayer instead of WKAudioFilePlayer the setup for MPRemoteCommandCenter working fine.
The problem that I don't use local audio files!.. I stream 'm3u8' files using wowza which worked only with WKAudioFilePlayer.
And if I tried to stream using AVAudioPlayer I get this error
The operation couldn’t be completed. (OSStatus error 2003334207.)
So my issue now to get MPRemoteCommandCenter being configured while still using WKAudioFilePlayer, or find a way to stream using AVAudioPlayer ??

Is there a way to show lock screen controls using AVAudioEngine and AVAudioPlayerNode?

I am handling audio playback using AVAudioEngine and AVAudioPlayerNode in my app, and I want to implement remote controls. Background audio is configured and working.
Control center controls work, but the play/pause button does not update when I play/pause the music from inside the app. I am testing on a real device.
Control center screenshot
Here is my AVAudioSession setup code:
func setupAudioSession() {
UIApplication.shared.beginReceivingRemoteControlEvents()
do {
try AVAudioSession.sharedInstance().setActive(true)
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch let sessionError {
print("Failed to activate session:", sessionError)
}
}
MPRemoteCommandCenter setup:
func setupRemoteControl() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
self.audioPlayerNode.play()
return .success
}
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
self.audioPlayerNode.pause()
return .success
}
}
Lock screen controls - never appeared.
So here is the solution to my problem, I was starting my AVAudioEngine together with its setup function called from viewDidLoad(), that was the issue, and i used .play()/.pause() methods on my AVAudioPlayerNode to manipulate the audio, however AVAudioPlayerNode does not emit master audio, outputNode of AVAudioEngine does.
So whenever you want to play/pause audio from inside your app or from command center, if you are using AVAudioEngine to handle audio in you application, don’t forget to call .stop()/.start() methods on your AVAudioEngine. Lock screen controls should show up and play/pause buttons should update properly in command center/lock screen even without a single property set to MPNowPlayingInfoCenter.default().nowPlayingInfo.
MPRemoteCommandCenter setup:
func setupRemoteControl() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
try? self.audioEngine.start()
return .success
}
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
self.audioEngine.stop()
return .success
}
}

Control audio outside app in iOS

I'm trying to control my audio player from outside of the app,
I started an av audio session but it's not playing on the background(worked fine on swift 3),
do{
myPlayer = AVPlayer(url: url!)
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback
)
do {
try audioSession.setActive(true)
}
}
catch {
print(error)
}
}
my main goal is to control play and pause like this:
So you are building an app that plays audio on the background using AVPlayer. You should use MPNowPlayingInfoCenter to display the song's metadata on the Lock Screen and Control Center, and use MPRemoteCommandCenter to control the previous/next/play/pause actions on the lock screen and control center.
Enable Background Mode for Audio, AirPlay and Picture in
Picture in your Target > Capabilities.
If you're streaming audio from the web, enable Background Mode for Background Fetch too.
Import AVKit and MediaPlayer dependencies.
Setup your AVPlayer with an AVPlayerItem:
guard let url = URL(string: "http://your.website.com/playlist.m3u8") else {
return
}
player = AVPlayer(url: url)
Setup your AVAudioSession:
private func setupAVAudioSession() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
debugPrint("AVAudioSession is Active and Category Playback is set")
UIApplication.shared.beginReceivingRemoteControlEvents()
setupCommandCenter()
} catch {
debugPrint("Error: \(error)")
}
}
Setup the InfoCenter and the RemoteCommandCenter:
private func setupCommandCenter() {
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyTitle: "Your App Name"]
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.playCommand.addTarget { [weak self] (event) -> MPRemoteCommandHandlerStatus in
self?.player.play()
return .success
}
commandCenter.pauseCommand.addTarget { [weak self] (event) -> MPRemoteCommandHandlerStatus in
self?.player.pause()
return .success
}
}
You can place the setupAVAudioSession() method in your viewDidLoad() method, or in any other place you need it.
If you need to place more info in the MPNowPlayingInfoCenter here's a list of all available properties: General Media Item Property Keys | Apple Developer Documentation

How to use thread to play next song by MPRemoteCommandCenter?

I want to use MPRemoteCommandCenter to control my music player app. And now it can play and pause music, but can not play next/previous song, only a poor chance can make it.
When user tap next song button in MPRemoteCommandCenter(e.g in the lock screen), it will call startExtendBGJob() function, then I ask for a thread to do the change song job(I think the bug is here, because I'm don't totally understand the background job's anatomy).
func startExtendBGJob(taskBlock: #escaping () -> Void) {
registerBackgroundTask()
DispatchQueue.global(qos: .userInitiated).async {
DLog("APP into BG")
DispatchQueue.global(qos: .default).async {
taskBlock()
}
while self.isPlaying == false || self.tmpPlayer == nil { // waiting for new avplayer been created.
Thread.sleep(forTimeInterval: 1)
}
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 10) {
self.endBackgroundTask()
}
}
}
func registerBackgroundTask() {
bgIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
[weak self] in
guard let strongSelf = self else { return }
strongSelf.endBackgroundTask()
})
assert(bgIdentifier != UIBackgroundTaskInvalid)
}
func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(bgIdentifier)
bgIdentifier = UIBackgroundTaskInvalid
isExtendingBGJob = false
DLog("App exit BG!")
}
In startNextPlay() function just finding the next song's url, and prepareToPlay() is for creating a new AVPlayer to play next song.
self.tmpPlayer = AVPlayer(url: streamURL)
I'm not english native spearker, thank you so much to read here if you understand what I'm talking about :]. Any help is welcome.
Sorry, It's my fault. It doesn't need any backgournd job.
Just change something like following:
UIApplication.shared.beginReceivingRemoteControlEvents()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
DLog("fail to set category: \(error)")
}
Make sure it's AVAudioSessionCategoryPlayback

Resources