Why is AVPlayer not continuing to next song while in background? - ios

I have been streaming music from remote source using AVPlayer. I get URLs, use one to create an AVPlayerItem, which i then associate with my instance of AVPlayer. I add an observer to the item that I associate with the player to observe when the item finishes playing ( AVPlayerItemDidPlayToEndTimeNotification ). When the observer notifies me at the item end, I then create a new AVPlayerItem and do it all over again. This works well in the foreground AND in the background on iOS 9.2.
Problem: Since I have updated to iOS 9.3 this does not work in the background. Here is the relevant code:
var portionToBurffer = Double()
var player = AVPlayer()
func prepareAudioPlayer(songNSURL: NSURL, portionOfSongToBuffer: Double){
self.portionToBuffer = portionOfSongToBuffer
//create AVPlayerItem
let createdItem = AVPlayerItem(URL: songNSURL)
//Associate createdItem with AVPlayer
player = AVPlayer(playerItem: createdItem)
//Add item end observer
NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerItemDidReachEnd:", name: AVPlayerItemDidPlayToEndTimeNotification, object: player.currentItem)
//Use KVO to see how much is loaded
player.currentItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .New, context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "loadedTimeRanges" {
if let loadedRangeAsNSValueArray = player.currentItem?.loadedTimeRanges {
let loadedRangeAsCMTimeRange = loadedRangeAsNSValueArray[0].CMTimeRangeValue
let endPointLoaded = CMTimeRangeGetEnd(loadedRangeAsCMTimeRange)
let secondsLoaded = CMTimeGetSeconds(endPointLoaded)
print("the endPointLoaded is \(secondsLoaded) and the duration is \(CMTimeGetSeconds((player.currentItem?.duration)!))")
if secondsLoaded >= portionToBuffer {
player.currentItem?.removeObserver(self, forKeyPath: "loadedTimeRanges")
player.play()
}
}
}
}
func playerItemDidReachEnd(notification: NSNotification){
recievedItemEndNotification()
}
func recievedItemEndNotification() {
//register background task
bgTasker.registerBackgroundTask()
if session.playlistSongIndex == session.playlistSongTitles.count-1 {
session.playlistSongIndex = 0
} else {
session.playlistSongIndex += 1
}
prepareAudioPlayer(songURL: session.songURLs[session.playlistSongIndex], portionOfSongToBuffer: 30.00)
}
I have set breakpoints to see that player.play() IS being called when in the background. When i print player.rate it reads 0.0. I have checked the property playbackLikelyToKeepUp of the AVPlayerItem and it is true. I have confirmed also that the new URL is successfully used to create the new AVPlayerItem and associated with the AVPlayer when the app is in the background. I have turned audio and airplay background capabilities on and I have even opened up a finite length background task (in code above as bgTasker.registerBackgroundTask). No idea what is going on.
I found THIS but i'm not sure it helps. Any advice would be great, thanks

When the observer notifies me at the item end, I then create a new AVPlayerItem and do it all over again
But the problem is that meanwhile play stops, and the rule is that background playing is permitted only so long as you were playing in the foreground and continue to play in the background.
I would suggest using AVQueuePlayer instead of AVPlayer. This will allow you to enqueue the next item while the current item is still playing — and thus, we may hope, this will count as continuing to play.

I encountered the similar problem, and I searched lots of websites on google, but didn't find the answer.
The Phenomenon
The problem of my app is that when I start play an audio, and turn the app to background, it will finish the playing of the current audio, and when playing the second audio, it will load some data, but then it stopped, if I turn the app to foreground, it will play the audio.
Solution
My solution is to add the following call.
UIApplication.shared.beginReceivingRemoteControlEvents()
So enable background audio capabilities is not enough, we need to begin receiving remote controller events.

Related

How to track the number of loops in AVAudioFoundation

I am making an audio player in my iOS project.
I setup the play(audioOfUrl:URL, for times:Int)method by passing the name of the url and how many times the audio file will be play as following:
func play(audioOfUrl:URL, for times:Int) {
let urlPath = audioOfUrl
loopsLeftOver = times
do {
let audio = try AVAudioPlayer.init(contentsOf: urlPath)
audioPlayer = audio
audio.play()
audio.numberOfLoops = times - 1
} catch let error {
print(error.localizedDescription)
}
}
there will be a pause() and resume() as well, What I need to do is keep track of how many loops the audio player leftover.
For example, if I call the play method like this:
play(audioOfUrl:audioUrl, for times:5)
I need to keep eyes on the times leftover as the audio player running.
I try to use the AVAudioPlayerDelegate method, but it is not working, how to keep eyes on the audio.numberOfLoops? Thanks in advance.
You can subscribe for AVPlayerItemDidPlayToEndTime notification and count how many times the handler of this notification has been called.
NotificationCenter.default.addObserver(self, selector:#selector(playerItemDidReachEnd(_:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: audioPlayer.currentItem)

AVAudioPlayer being interrupted by MPMusicPlayerController

I am writing an app that uses both AVAudioPlayer to play local mp3 files and MPMusicPlayerController to play system / Apple music / iTunes music.
I am maintaining a custom nowPlayingMP3Item that is being set after I play the song using AVAudioPlayer.
I could check the playback state of the MusicController, but I'd need to reload the UI if it changed from mp3 to system music...
Is there any way to observe (maybe the audio session) or something and determine whether mp3 or system music is playing and update the UI accordingly?
Thanks for any help
When there is an interruption,the audio will be paused and your audio session will been deactivated. If your app contains playing interface, you will need to update the interface. When the interruption ends, you can activate audio session and resume playback of the audio.
NotificationCenter.default.addObserver( forName: .AVAudioSessionInterruption, object: nil, queue: nil) { n in
let why = AVAudioSessionInterruptionType( rawValue: n.userInfo![ AVAudioSessionInterruptionTypeKey] as! UInt)!
if why = = .began {
// began
}
else {
// ended
}
}

Ios AVPlayer - Wait for buffering [duplicate]

I am streaming audio by using AVPlayer. It is working well. But Now i need to make a slider to move audio forward and backward, like typical music player. I used the function seekToTime which works great with local audio file. But when i stream song from web url, the function stops audio when I forward slider with a large range.
My guess is that the song is being downloaded and if i move slider forward with large range then may be system has not downloaded the song data packets for that time, so it halts player.
Lets say i just hit a button to stream song but now i move slider immediately at 0 to 100th second. In this case system is not working and stops the player. Due to lack of data packets for that time.
Has anyone know how to overcome this problem or is there any other approach. I am using SWIFT for development. I am up to use any library as well if it would work in swift. This is my function:
func forward () {
timeGap = slider.value
let preferredTimeScale : Int32 = 1
let targetTime : CMTime = CMTimeMake(timeGap, preferredTimeScale)
player.seekToTime(targetTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
}
Thanks in advance.
This answer is heavily based on this one : https://stackoverflow.com/a/7195954/765298 . Any and all credit should go there as this is merely a translation to swift.
You are right to assume that when seeking far forward to a part that has not been buffered yet the player stops. The thing is it still buffers the data, but doesn't start automatically when it is ready. So, to rephrase the linked answer to swift :
To setup your observers :
player.currentItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .New, context: nil)
player.currentItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .New, context: nil)
Note, that in order to be able to observe values, the passed self needs to inherit from NSObject.
And to handle them :
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard keyPath != nil else { // a safety precaution
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
return
}
switch keyPath! {
case "playbackBufferEmpty" :
if player.currentItem!.playbackBufferEmpty {
print("no buffer")
// do something here to inform the user that the file is buffering
}
case "playbackLikelyToKeepUp" :
if player.currentItem!.playbackLikelyToKeepUp {
self.player.play()
// remove the buffering inidcator if you added it
}
}
}
You can also get information about avaiable time ranges from the currently playing AVPlayerItem (you can acces it via player.currentItem if you haven't created it yourself). This enables you to indicate to user which parts of the file are ready to go.
As always you can read some more in the docs : AVPlayerItem and AVPlayer
To read more about key-value observing (KVO) : here

SeekToTime in AVPlayer stops playing streaming audio, when forward

I am streaming audio by using AVPlayer. It is working well. But Now i need to make a slider to move audio forward and backward, like typical music player. I used the function seekToTime which works great with local audio file. But when i stream song from web url, the function stops audio when I forward slider with a large range.
My guess is that the song is being downloaded and if i move slider forward with large range then may be system has not downloaded the song data packets for that time, so it halts player.
Lets say i just hit a button to stream song but now i move slider immediately at 0 to 100th second. In this case system is not working and stops the player. Due to lack of data packets for that time.
Has anyone know how to overcome this problem or is there any other approach. I am using SWIFT for development. I am up to use any library as well if it would work in swift. This is my function:
func forward () {
timeGap = slider.value
let preferredTimeScale : Int32 = 1
let targetTime : CMTime = CMTimeMake(timeGap, preferredTimeScale)
player.seekToTime(targetTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
}
Thanks in advance.
This answer is heavily based on this one : https://stackoverflow.com/a/7195954/765298 . Any and all credit should go there as this is merely a translation to swift.
You are right to assume that when seeking far forward to a part that has not been buffered yet the player stops. The thing is it still buffers the data, but doesn't start automatically when it is ready. So, to rephrase the linked answer to swift :
To setup your observers :
player.currentItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .New, context: nil)
player.currentItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .New, context: nil)
Note, that in order to be able to observe values, the passed self needs to inherit from NSObject.
And to handle them :
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard keyPath != nil else { // a safety precaution
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
return
}
switch keyPath! {
case "playbackBufferEmpty" :
if player.currentItem!.playbackBufferEmpty {
print("no buffer")
// do something here to inform the user that the file is buffering
}
case "playbackLikelyToKeepUp" :
if player.currentItem!.playbackLikelyToKeepUp {
self.player.play()
// remove the buffering inidcator if you added it
}
}
}
You can also get information about avaiable time ranges from the currently playing AVPlayerItem (you can acces it via player.currentItem if you haven't created it yourself). This enables you to indicate to user which parts of the file are ready to go.
As always you can read some more in the docs : AVPlayerItem and AVPlayer
To read more about key-value observing (KVO) : here

MPMusicPlayerController.play() not working after killing iOS music app

I have an app that allows you to search for the title of a song in the music library and play it. I play the selected song using this code:
func playSongByPersistentID(id: Int) { //id is the persistent id of chosen song
let predicate = MPMediaPropertyPredicate(value: id, forProperty: MPMediaItemPropertyPersistentID)
let songQuery = MPMediaQuery()
songQuery.addFilterPredicate(predicate)
if songQuery.items?.count < 1 {
print("Could Not find song") // make alert for this
return
} else {
print("gonna play \(songQuery.items?[0].title)")
}
musicPlayer.prepareToPlay()
musicPlayer.setQueueWithItemCollection(songQuery.collections![0])
musicPlayer.play()
}
The above function gets called in tableView(:didSelectRowAtIndexPath). I have confirmed that the correct ID and song title is retrieved when a song is selected.
Here is my problem. If I go into my app and select a song to play after killing the iOS music app, the song does not play. If I then choose a different song, that different song plays no problem. If I choose the same song over and over again it never plays.
musicPlayer is a systemMusicPlayer declared in my class.
Is this an iOS bug? I have no idea what is going on.
Here is the workaround I found.
What it does is set a timer once I try to start playing the music. That timer calls a function that tests if the music is playing. If the music is playing, the timer is invalidated. If not, it re-queues the item I want to play (a collection of items in this example) and tries to play again.
I've pulled this code from my app and abstracted it, so it may not compile as is but hopefully it gets the point across. I'm going to file this bug with Apple (after creating a small sample project) and recommend you do the same.
func playMusic()
{
musicPlayer = MPMusicPlayerController.applicationMusicPlayer()
musicPlayer.setQueueWithItemCollection(MPMediaItemCollection(items: songsForPlayingWithMusicPlayer))
musicPlayer.play()
testForMusicPlayingTimer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(1), target: self, selector: "testForMusicPlaying", userInfo: nil, repeats: false)
}
func testForMusicPlaying()
{
if musicPlayer.playbackState != .Playing
{
testForMusicPlayingTimer.invalidate()
musicPlayer = MPMusicPlayerController.applicationMusicPlayer()
musicPlayer.setQueueWithItemCollection(MPMediaItemCollection(items: songsForPlayingWithMusicPlayer))
musicPlayer.play()
testForMusicPlayingTimer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(1), target: self, selector: "testForMusicPlaying", userInfo: nil, repeats: false)
}
else
{
testForMusicPlayingTimer.invalidate()
}
}

Resources