2

I am having difficulty with my music app. The idea is to tap a button and play more songs from the artist that is currently playing. When I am IN the app and I hit next song it works fine. However if I let the song end naturally the app would play a random song. So I added logic to say if the songs remaining time is 0 then play next song using media players func because I know that works. This solved it except if the app is in the background. I tried to keep the app alive if it is in the background but I guess that is not working.

Every time I think I have solved something it the issue comes back.

What I expect to happen is lock on an Artist, close app to background and when the song ends play another song from Artist.

What actually happens is when I lock the artist and close app to background is the song will end and sometimes it will play the right song. Sometimes it will not. HOWEVER when it plays the wrong song and I open the app back up it ends the currently (Wrong) playing song and starts playing a song by the proper artist

to be clear, I think it’s unwise to need logic that runs when the song time remaining is 0 to skip to next song but for whatever reason I need it

I have set the Capabilities to background fetch and Audio Airplay and picture in picture

I have tried to only post relevant code in order..

I have a property

let mediaPlayer = MPMusicPlayerController.systemMusicPlayer
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid

In my ViewDidLoad

try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient)
try? AVAudioSession.sharedInstance().setActive(true)


DispatchQueue.main.async {
  self.clearSongInfo()
  MediaManager.shared.getAllSongs { (songs) in
    guard let theSongs = songs else {
      return
    }
    self.mediaPlayer.nowPlayingItem = nil

    self.newSongs = theSongs.filter({ (item) -> Bool in
      return !MediaManager.shared.playedSongs.contains(item)
    })
    self.aSongIsInChamber = true
    self.mediaPlayer.setQueue(with: MPMediaItemCollection(items: self.newSongs.shuffled())
    )
    self.mediaPlayer.shuffleMode = .off
    self.mediaPlayer.repeatMode = .none

  }
  NotificationCenter.default.addObserver(self, selector: #selector(self.songChanged(_:)), name: NSNotification.Name.MPMusicPlayerControllerNowPlayingItemDidChange, object: self.mediaPlayer)
  self.mediaPlayer.beginGeneratingPlaybackNotifications()
}

I have a func that updates the played time and remaining time and in that I have

if Int(self.songProgressSlider.maximumValue - self.songProgressSlider.value) < 1 {
  print("song ended naturally, skipped")
  mediaPlayer.prepareToPlay(completionHandler: { (error) in
    DispatchQueue.main.async {
      self.mediaPlayer.skipToNextItem()
    }
  })
}

When I play the music I have a bool isPlaying

If it is then the time is running and I have this

let app = UIApplication.shared
  var task: UIBackgroundTaskIdentifier?
  task  = app.beginBackgroundTask {
    app.endBackgroundTask(task!)
  }

I have that to keep the timer on in the background so it will play the next song when the time remaining is 0

Now to lock onto an Artist I have the following code that executes on button tap

let artistPredicate: MPMediaPropertyPredicate  = MPMediaPropertyPredicate(value: nowPlaying.artist, forProperty: MPMediaItemPropertyArtist)
      let query: MPMediaQuery = MPMediaQuery.artists()
      let musicPlayerController: MPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer

      query.addFilterPredicate(artistPredicate)
      musicPlayerController.setQueue(with: query)
RubberDucky4444
  • 2,330
  • 5
  • 38
  • 70
  • @solenoid I want to start playing music on shuffle and then on button tap set the queue to be songs by that artist and then later if tapped again remove that artist lock – RubberDucky4444 Mar 19 '18 at 02:19
  • Ahh - you want the current song to expire than IF button = pressed, start playing songs by the artist. If you send a new queue with the current song playing does it stop playback of the current song? I don't think you should have to do anything else, except change the queue again when the button leaves the on state. – solenoid Mar 19 '18 at 02:24
  • @solenoid no the current song does not expire it just captures the artist and then the next song should be by same artist. When the user wants to stop from that artist I have a similar method that removes the lock. It works if app is in foreground – RubberDucky4444 Mar 19 '18 at 02:26
  • In your artistPredicate, your value - is that actually returning what you want? I dont see it defined anywhere, I use: MPMusicPlayerController.systemMusicPlayer().nowPlayingItem.albumArtist – solenoid Mar 19 '18 at 02:33
  • @solenoid yes it does. I can try that when I get back home but like I’ve said it works. Just not when app is in background – RubberDucky4444 Mar 19 '18 at 02:35
  • From the sounds of it something is not able to run in the background; try just setting a queue, pressing the artist button and setting a new queue, then going to the background and test it (without any of the other things). In an app of mine I basically do this with no background queue issues. – solenoid Mar 19 '18 at 02:54
  • @solenoid I’ve tried that which is why I’m here. I’ve set the queue and it shouldn’t matter if the app is background or foreground but when app enters the background it screws with the queue – RubberDucky4444 Mar 19 '18 at 03:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/167066/discussion-between-solenoid-and-rubberducky4444). – solenoid Mar 19 '18 at 03:05
  • @solenoid solved it kinda – RubberDucky4444 Mar 20 '18 at 19:29

2 Answers2

1

I find that setting the queue does not "take" until you say prepareToPlay (literally or implicitly by saying play). The current edition of my book says:

My experience is that the player can behave in unexpected ways if you don't ask it to play, or at least prepareToPlay, immediately after setting the queue. Apparently the queue does not actually take effect until you do that.

You have also said (in a comment):

ideally I would like the song to finish first and then go onto the queue I have

Okay, but then perhaps what you want here is the (new in iOS 11) append feature that allows you modify the queue without replacing it entirely.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • ideally I would like the song to finish first and then go onto the queue I have set. using prepareToPlay interrupts the song just as my answer does. – RubberDucky4444 Mar 20 '18 at 19:38
  • it gets tricky because when the user is tired of that artist he/she can tap the button again and remove the lock. I would then need to remove that artist, but only the songs that have already been played (if the full song list by that artist wasn't played). Theres no remove method that I can see to undo the append and if I am appending then I am adding that artist to the queue twice? – RubberDucky4444 Mar 20 '18 at 20:32
0

I found a solution that isn't 100% what I wanted, but at this point I'll take it.

On the IBAction where I lock onto an artist I was originally just

self.mediaPlayer.prepareToPlay { error in
  let artistPredicate: MPMediaPropertyPredicate  = MPMediaPropertyPredicate(value: nowPlaying.artist, forProperty: MPMediaItemPropertyArtist)
  let query: MPMediaQuery = MPMediaQuery.artists()
  let musicPlayerController: MPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer

  query.addFilterPredicate(artistPredicate)
  musicPlayerController.setQueue(with: query)
}

What I have done to fix my issue is added

musicPlayerController.play()

after I set the queue.

The downside to this approach is that if I am midway through a song and I tap the button to lock the artist the app immediately ends the now playing song and goes to songs by that artist and I would rather the song finish playing and THEN go the songs I want.

If anybody can think of a way to use play() AND let the song ends naturally ill mark that answer as correct...

NOTE I never wanted to use the logic that checks how long is left in a song and I never wanted to use background code. So I got rid of that and it still works with new idea.

RubberDucky4444
  • 2,330
  • 5
  • 38
  • 70
  • Try saying `prepareToPlay()` right after setting the queue instead. I find that the new queue does not "take" until you do at least that. And this way, you won't be stuck with a play you may not want. – matt Mar 20 '18 at 19:34
  • 1
    @matt I found after beta 3 or 4 prepareToPlay() causes the music to start playing immediately. I just tried it again and it has the same effect as using play() where it interrupts the current song – RubberDucky4444 Mar 20 '18 at 19:36
  • Okay, but new in iOS 11 you can `append` to the queue instead of setting it, and that might be what you want here. – matt Mar 20 '18 at 19:39