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

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()
}
}

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)

iOS turn based match, push notifications not working, GKTurnBasedEventListener functions not called

In my iOS turn based match, I'm trying to receive notifications and to get the
public func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool)
to be called, with no success.
I register my view model to the local player
GKLocalPlayer.localPlayer().register(self)
and I would expect that to fire after the other player executes
func endTurn(withNextParticipants nextParticipants: [GKTurnBasedParticipant], turnTimeout timeout: TimeInterval, match matchData: Data, completionHandler: ((Error?) -> Swift.Void)? = nil)
but no success.
If I force a reload of the matchData then I will get the data the second player just submitted. So the endTurn works correctly.
Is there something I'm doing wrong?
Update:
So I create a new project, copied all my files over,
in the capabilities only Game Center was enabled.
When developing it was working perfect, I had two devices attached (with different apple IDs). Notifications were working and Turnbasedlistener was firing.
As soon as I released it for internal testing it stopped working!!!
I had very similar issue. My solution was to manually recheck my status while waiting for my turn.
FIrst, I defined global variable var gcBugTimer: Timer
In endTurn(withNextParticipants:turnTimeOut:match:completionHan‌​dler:) completion handler:
let interval = 5.0
self.gcBugTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(self.isMatchActive), userInfo: nil, repeats: true)
self.gcBugTimer.tolerance = 1.0
Code above also should be called in case when a player is joying to a new match and other player in a turn.
Then timer method:
func isMatchActive() {
// currentMatch - global variable contains information about current match
GKTurnBasedMatch.load(withID: currentMatch.matchID!) { (match, error) in
if match != nil {
let participant = match?.currentParticipant
let localPlayer = GKLocalPlayer.localPlayer()
if localPlayer.playerID == participant?.player?.playerID {
self.player(localPlayer, receivedTurnEventFor: match!, didBecomeActive: false)
}
} else {
print(error?.localizedDescription ?? "")
}
}
}
And I add following code at the very beginning of player(_:receivedTurnEventFor:didBecomeActive):
if gcBugTimer != nil && gcBugTimer.isValid {
gcBugTimer.invalidate()
}
What ended up working for me, was to test on an actual device, rather than in simulator. The receivedTurnEvents function doesn't seem to work in simulator.
Grigory's work around is great for testing with simulator.

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

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.

Detect how long a sound is in iOS

I'm using the audio metering in AVFoundation and I wanted to know if there's a way to figure out how long a sound is. I have my audio recorder setup like this:
func startMeterTimer() {
levelTimer?.invalidate()
levelTimer = CADisplayLink(target: self, selector: "updateMeter")
levelTimer?.frameInterval = 5
levelTimer?.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
}
func stopMeterTimer() {
levelTimer?.invalidate()
levelTimer = nil
}
func updateMeter() {
readLevels()
let avgPower = audioRecorder?.averagePowerForChannel(0)
let peakPower = audioRecorder?.peakPowerForChannel(0)
if peakPower >= -2 {
print("CLAP DETECTED")
soundLogicDelegate?.clapDetected()
}
// print("\(avgPower) + \(peakPower)")
}
func record() -> Bool {
return (audioRecorder?.record())!
}
record and startMeterTImer are called and when I clap my hand, even though I only clap once, I get many print statements of CLAP DETECTED. I was wondering if there was a way to measure how long a method is called from the first time it's called and the last time it's called.
Use the audioRecorderDidFinishRecording delegate method, one of it's parameters is an instance of AVAudioRecorder which has currentTime property:
The time, in seconds, since the beginning of the recording.
(read-only)
If you want to check recording time each time updateMeter() is called use audioRecorder?.currentTime

Play audio file once every time NSTimer decrements?

I'm working in Swift and I have an NSTimer that counts down from 3, 2, 1. I want to play an audio file every time this timer decrements, so that the timer would show 3 and the audio would play once, then it flips to 2 and the audio plays once, finally it goes to 1 and it plays once.
This is how I've created the timer and tried to do that:
var path2 = NSBundle.mainBundle().pathForResource("splatter1", ofType: "wav")
var soundTrack2 = AVAudioPlayer()
func timeToMoveOn() {
let loadingDelay = 0.01 * Double(NSEC_PER_SEC)
let loadingTime = dispatch_time(DISPATCH_TIME_NOW, Int64(loadingDelay))
dispatch_after(loadingTime, dispatch_get_main_queue()) {
//After delay
self.performSegueWithIdentifier("goToGameScene", sender: self)
}
}
func splatterSound() {
soundTrack2 = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: path2!), error: nil)
soundTrack2.numberOfLoops = 1
soundTrack2.volume = 0.35
soundTrack2.play()
}
func updateCounter() {
countdownTestLabel.text = String(counter--)
splatterSound()
if counter == 0 {
countdown.invalidate()
//Then trigger segue to Game Scene view
timeToMoveOn()
}
}
In view did load:
counter = 3
countdown = NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: Selector("updateCounter"), userInfo: nil, repeats: true)
My problem is that the audio file plays every time the timer decrements, but it plays multiple times. For example it will one time, then a second time, then a third fourth and fifth time all in rapid succession. It shouldn't do that.
How can I fix this so that it only plays when the timer decrements?
Get rid of the soundTrack2.numberOfLoops = 1 bit. The numberOfLoops setting is the number of times to repeat the sound. So the first time your timer fires, you queue up your sound to play twice in a row.
Then one second later, you queue up the sound to play twice in a row again.
Then, yet one second later, you queue up your sound to play another twice in a row.
If you eliminate that line, the sound will default to numberOfLoops=0, or just playing the sound once (which is what you want.)

Resources