setup MPRemoteCommandCenter in watchOS Now Playing with WKAudioFilePlayer - ios

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

Related

How to run a function in a for loop sequentially

I have a set of buttons in a stack view. Each button when pressed plays a different sound. I have a separate button (loop button) that when pressed calls the loopButtonPressed function. My goal is that when this loop button is pressed, it will loop through the subviews that are buttons in this stack view and play each of the sounds sequentially in order using the soundButtonPressed function. I saw a method that I implemented below using the run() function which sets each consecutive function to run after a given amount of time. Although this kind of works it is not a great solution because the sound files are of varying length. I was thinking there may be a way to do this using dispatch groups, which I don't fully understand. If I take away the run function, it will only play the sound of the last button in the stack view. I am using AVFoundation to play the wav files as well. I appreciate any advice or direction, thanks.
func run(after seconds: Int, completion: #escaping () -> Void) {
let deadline = DispatchTime.now() + .milliseconds(seconds)
DispatchQueue.main.asyncAfter(deadline: deadline) {
completion()
}
}
#objc func loopButtonPressed(_ sender: UIButton) {
var i = 1
for case let button as UIButton in self.colorBubblesStackView.subviews {
run(after: 800*i) {
self.soundButtonPressed(sender: button)
}
i += 1
}
}
My soundButtonPressed function is just a switch statement where each case calls the function playSound() with the correct sound file name. Here is the playSound function:
func playSound(_ soundFileName: String) {
guard let url = Bundle.main.url(forResource: soundFileName, withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
guard let player = player else { return }
player.play()
} catch let error {
print(error.localizedDescription)
}
}
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
player?.delegate = self
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is working now! printed "finished"!
}
confirm to protocol
class ViewController: UIViewController,AVAudioPlayerDelegate {
and instead of looping add each button a Tag and start from first tag sat 100 . Then when the call back obtained for player finished playing play the next File with new icremented Tag say 101
Try AVQueuePlayer
A player used to play a number of items in sequence.
#objc func loopButtonPressed(_ sender: UIButton) {
let allUrls = allSongs.compactMap { Bundle.main.url(forResource: $0, withExtension: "wav") }
let items = allUrls.map { AVPlayerItem(url: $0) }
let queuePlayer = AVQueuePlayer(items: items)
queuePlayer.play()
}

AVAudioPlayer 'forgets' that its playing when triggered by MPRemoteCommand

I'm trying to play an audiofile and control it's playback via the Remote Command Centre available on the lock screen.
If I do the following:
Begin playback
Pause playback
Lock device
Begin playback from lockscreen (MPRemoteCommandCenter)
It is then impossible to pause playback from lockscreen. The button flickers and nothing happens.
How can I fix this?
Further details below:
It appears that when attempting to pause the audio, the AVAudioPlayer returns 'false' for audioPlayer.isPlaying.
This occurs on iOS13.1 on my iPhoneXR, iPhoneSE and iPhone8. I have no other devices to test against
The logs indicate that AVAudioPlayer.isPlaying initially returns true when playback is started, but subsequently returns false. The player's currentTime also appears stuck at around the time playback was started.
My entire view controller is below (~100 lines). This is the minimum necessary to reproduce the problem.
The this example project demonstrating the error is also available on Github here.
import UIKit
import MediaPlayer
class ViewController: UIViewController {
#IBOutlet weak var playPauseButton: UIButton!
#IBAction func playPauseButtonTap(_ sender: Any) {
if self.audioPlayer.isPlaying {
pause()
} else {
play()
}
}
private var audioPlayer: AVAudioPlayer!
private var hasPlayed = false
override func viewDidLoad() {
super.viewDidLoad()
let fileUrl = Bundle.main.url(forResource: "temp/intro", withExtension: ".mp3")
try! self.audioPlayer = AVAudioPlayer(contentsOf: fileUrl!)
let audioSession = AVAudioSession.sharedInstance()
do { // play on speakers if headphones not plugged in
try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
} catch let error as NSError {
print("Override headphones failed, probably because none are available: \(error.localizedDescription)")
}
do {
try audioSession.setCategory(.playback, mode: .spokenAudio)
try audioSession.setActive(true)
} catch let error as NSError {
print("Warning: Setting audio category to .playback|.spokenAudio failed: \(error.localizedDescription)")
}
playPauseButton.setTitle("Play", for: .normal)
}
func play() {
playPauseButton.setTitle("Pause", for: .normal)
self.audioPlayer.play()
if(!hasPlayed){
self.setupRemoteTransportControls()
self.hasPlayed = true
}
}
func pause() {
playPauseButton.setTitle("Play", for: .normal)
self.audioPlayer.pause()
}
// MARK: Remote Transport Protocol
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(".......................")
print(self.audioPlayer.currentTime)
let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
print("\(address) not playing: \(!self.audioPlayer.isPlaying)")
guard !self.audioPlayer.isPlaying else { return .commandFailed }
print("attempting to play")
let success = self.audioPlayer.play()
print("play() invoked with success \(success)")
print("now playing \(self.audioPlayer.isPlaying)")
return success ? .success : .commandFailed
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(".......................")
print(self.audioPlayer.currentTime)
let address = Unmanaged.passUnretained(self.audioPlayer).toOpaque()
print("\(address) playing: \(self.audioPlayer.isPlaying)")
guard self.audioPlayer.isPlaying else { return .commandFailed }
print("attempting to pause")
self.pause()
print("pause() invoked")
return .success
}
private func setupRemoteTransportControls() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget(self, action: #selector(self.handlePlay))
commandCenter.pauseCommand.addTarget(self, action: #selector(self.handlePause))
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = "Major title"
nowPlayingInfo[MPMediaItemPropertyTitle] = "Minor Title"
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.audioPlayer.duration
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
This logs the following (with my // comments added):
.......................
1.438140589569161 // audio was paused here
0x0000000283361cc0 not playing: true // player correctly says its not playing
attempting to play // so it'll start to play
play() invoked with success true // play() successfully invoked
now playing true // and the player correctly reports it's playing
.......................
1.4954875283446711 // The player thinks it's being playing for about half a second
0x0000000283361cc0 playing: false // and has now paused??? WTF?
.......................
1.4954875283446711 // but there's definitely sound coming from the speakers. It has **NOT** paused.
0x0000000283361cc0 playing: false // yet it thinks it's paused?
// note that the memory addresses are the same. This seems to be the same player. ='(
I'm at my wits' end. Help me StackOverflow—You're my only hope.
Edits: I've also tried
Always returning .success
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
guard !self.audioPlayer.isPlaying else { return .success }
self.audioPlayer.play()
return .success
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
print(self.audioPlayer.isPlaying)
guard self.audioPlayer.isPlaying else { return .success }
self.pause()
return .success
}
Ignoring the audioPlayer state and just doing as the remote command centre says
#objc private func handlePlay(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
self.audioPlayer.play()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
return .success
}
#objc private func handlePause(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
self.audioPlayer.pause()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.audioPlayer.currentTime
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.audioPlayer.rate
return .success
}
Both of these result in the bug persisting the first time pause is tapped on the lock screen. Subsequent taps reset the audio to the original paused position and then work normally.
Update
Replacing AVAudioPlayer with AVPlayer appears to make the problem go away entirely! I'm reasonably sure this is a bug in AVAudioPlayer now.
The necessary steps to switch to AVPlayer are in this public diff
I've used one of my developer-question tickets and submitted this question to Apple. I'll post an answer when I hear back from them.
Update 2
Apple Dev Support confirmed that as of 5 Dec 2019 there's no known workaround for this issue. I've submitted an issue to feedbackassistant.apple.com and will update this answer when something changes.
This is indeed a bug in AVAudioPlayer.
Another workaround if you dont want to switch to AVPlayer is to simply check if playing before pausing and if not, call play just before pause. It's not pretty but it works:
if (!self.player.isPlaying) [self.player play];
[self.player pause];

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

iOS Swift - MPCommandCenter & NowPlayingInfoCenter Controlling Music on Lock Screen

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.

Resources