Play/Pause and Elapsed Time not updating in iOS command center properly - ios

I have a video player that can play from the iOS command center and lock screen. When I toggle a play/pause button in my app, it should update the play/pause button in the command center (MPRemoteCommandCenter) by updating the nowPlayingInfo (MPNowPlayingInfoCenter). I'm not sure why it's not updating.
For example, if I pause the video with a custom button in my app, the command center still shows the pause button (meaning the video is still playing which is wrong.)
This is how I update the nowPlayingInfo:
func updateMPNowPlayingInforCenterMetadata() {
guard video != nil else {
nowPlayingInfoCenter.nowPlayingInfo = nil
return
}
var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]()
let image: UIImage
if let placeholderLocalURL = video.placeholderLocalURL, let placeholderImage = UIImage(contentsOfFile: placeholderLocalURL.path) {
image = placeholderImage
} else {
image = UIImage()
}
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ -> UIImage in
return image
})
nowPlayingInfo[MPMediaItemPropertyTitle] = video.title
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = video.creator?.name ?? " "
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Float(video.duration)
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Float(currentTime) // CMTimeGetSeconds(player.currentItem!.currentTime())
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = player.rate
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
if player.rate == 0.0 {
state = .paused
} else {
state = .playing
}
}
With KVO, when the player's rate changes, I call this function:
// MARK: - Key-Value Observing Method
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &assetPlaybackManagerKVOContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
} else if keyPath == #keyPath(AVPlayer.rate) {
updateMPNowPlayingInforCenterMetadata()
}
}
Any thoughts?
UPDATE
I found a solution though but not perfect in my case. So in my app I have 2 view controller's. Let's call them FeedVC and PlayerVC. So FeedVC has AVPlayer's that are always playing but are muted. If you click on one of them, then the PlayerVC is created and plays the full video. If I pause the AVPlayer's in FeedVC before going to PlayerVC then the "play/pause" button in the NowPlayingInfoCenter works perfectly!
Is there a way to make this work without having to pause the videos in the FeedVC?
Another issue is that the elapsed time keeps counting if I don't pause the players in the FeedVC. It seems that if multiple players are playing, the play/pause button and elapsed time are incorrect.

When you setting the dictionary for nowPlayingInfo you will want to set the MPNowPlayingInfoPropertyPlaybackRate value appropriately. The MPNowPlayingInfoCenter is expecting either a 1.0 (playing) or 0.0 (not playing) value as a Double wrapped in a NSNumber object. Below you will find the code for how I'm setting the nowPlayingInfo in my project.
func setNowPlayingMediaWith(song: SUSong, currentTime: Double?) {
var playingInfo:[String: Any] = [:]
if let title = song.title {
playingInfo[MPMediaItemPropertyTitle] = title
}
if let songID = song.id {
playingInfo[MPMediaItemPropertyPersistentID] = songID
}
if let artist = song.artist, let artistName = artist.name {
playingInfo[MPMediaItemPropertyArtist] = artistName
}
if let album = song.album, let albumTitle = album.title {
var artwork:MPMediaItemArtwork? = nil
if let album = song.album, let artworkData = album.artwork {
artwork = MPMediaItemArtwork(boundsSize: Constants.Library.Albums.thumbSize, requestHandler: { (size) -> UIImage in
return UIImage(data: artworkData as Data)!
})
}
if artwork != nil {
playingInfo[MPMediaItemPropertyArtwork] = artwork!
}
playingInfo[MPMediaItemPropertyAlbumTitle] = albumTitle
}
var rate:Double = 0.0
if let time = currentTime {
playingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time
playingInfo[MPMediaItemPropertyPlaybackDuration] = song.duration
rate = 1.0
}
playingInfo[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: rate)
playingInfo[MPNowPlayingInfoPropertyMediaType] = NSNumber(value: MPNowPlayingInfoMediaType.audio.rawValue)
MPNowPlayingInfoCenter.default().nowPlayingInfo = playingInfo
}
In this method I am passing the song that my player is has currently loaded. Whenever the user chooses to play or pause I call setNowPlayingMediaWith(song:currentTime:) to update the device's control console.
I keep track of the currentTime (Double) as a property of my player. If there is a currentTime passed in that means we are meant to be playing, so set the MPNowPlayingInfoPropertyPlaybackRate to 1.0 and set the MPNowPlayingInfoPropertyElapsedPlaybackTime to currentTime. This will set the current time to start automatically playing in the device's control console.
Likewise, if there is no currentTime passed then we have stopped or paused. In this case we set the MPNowPlayingInfoPropertyPlaybackRate to 0.0 and we do not include the MPNowPlayingInfoPropertyElapsedPlaybackTime.
Download my app to see this in action.
EDIT (answer to comments)
"Is there a way to make this work without having to pause the videos in the FeedVC"
Without seeing your code it is difficult to give you a definite answer. It would make sense though to pause any ongoing media before starting your PlayerVC's media.
"the elapsed time keeps counting"
The elapsed time will countdown the elapsed time based on an NSTimer property on NowPlayingInfoCenter. This timer will stop only when the timers value has reached the value you set in MPMediaItemPropertyPlaybackDuration, or when you update the MPNowPlayingInfoPropertyElapsedPlaybackTime.
My suggestion is to write a method that "clears out" the NowPlayingInfoCenter. This method should set the will set all of key values to either 0.0 or nil respectively. Call this "clear out" method each time before you play your media in PlayerVC. Then once you play from PlayerVC, set the NowPlayingInfoCenter key values like you would in the method I pasted in my answer to set the new values for the new playing media.

Related

Swift 4 The UISlider with AVPlayer doesn't slide

I am trying to control the audio with the UISlider, the print() statement prints the correct value and the app doesn't crash, however when I try to grab it and move the slider (the thumb tint), the UISlider doesn't slide but just moves a bit when I try to slide it ( like a tap ).
When I comment the 6th row the slider response correctly (But of course nothing happens).
var playerItem : AVPlayerItem?
var player : AVPlayer?
#IBAction func adjustSongProgress(_ sender: UISlider) {
player?.pause()
let floatTime = Float(CMTimeGetSeconds(player!.currentTime()))
sliderProgressOutlet.value = floatTime
print(floatTime)
player?.play()
}
 Fixed it by deleting AVPlayer and changing AVPlayerItem to AVAudioPlayer, then putting the song URL into data : `
//DOWNLOADS THE SONG IN THE URL AND PUTS IT IN DATA
var task: URLSessionTask? = nil
if let songUrl = songUrl {
task = URLSession.shared.dataTask(with: songUrl, completionHandler: { data, response, error in
// SELF.PLAYER IS STRONG PROPERTY
if let data = data {
self.playerItem = try? AVAudioPlayer(data: data)
self.playPause()
DispatchQueue.main.async {
self.sliderProgress()
}
}
})
task?.resume()`
Then I changed UISlider IBAction Value Changed to Touch Down and Touch Up Inside when I connected it to the ViewController:
// TOUCH DOWN
#IBAction func SliderTouchDown(_ sender: UISlider) {
playerItem?.pause()
}
//TOUCH UP INSIDE
#IBAction func SliderTouchUpInside(_ sender: UISlider) {
playerItem?.currentTime = TimeInterval(sliderProgressOutlet.value)
playerItem?.play()
}
iOS Slider takes value between 0 to 1. if CMTimeGetSeconds return value outside from 0 to 1 it will not set properly.
therefor you have to convert your Time range to slider range.
for ex : your video/audio length is 120 second and you want to move slider to 30 second.than you can calculate new value using below function.
OldRange = (OldMax - OldMin)
NewRange = NewMax - NewMin
NewValue = (((OldValue - OldMin) * NewRange) / OldRange) + NewMin
oldRange = 120 - 0
newRange = 1 - 0
New value = (30-0)*1/120+0 = 0.25

Video Player not loading upon return from modal

I am working on an iOS app with XCode 9.3 and Swift 4.
I have a setup where I have a screen, Episode view, with information on a particular video.
For a user that is not logged in, The screen contains:
an image at the top
underneath the image information about the video and a small description.
a Sign in button
The Sign In button opens a modal. Whenever a
user taps on this button, the user should be signed in and then return to the Episode view where the image should be replaced by a video
player (we use JW) with the respective episode.
I created a delegate that would call a method in the EpisodeViewController upon signing in:
func refreshTopView() {
subscribed = 1
setUpTopView()
print("View has refreshed")
}
Upon logging in, the delegate calls setUpTopView()
func setUpTopView() {
if subscribed == 1 {
// Hide the image view
episodeImage.isHidden = true
// Set up URL to video
let linktoJSON = "https://content.jwplatform.com/feeds/" + (episode?.interviewKey)! + ".json";
// Set video screen flag to true
videoScreen = true
// Get the feed for the video
getFeed(url: linktoJSON)
} else {
// Hide the container view for the video
containerView.isHidden = true
// Get the show path and image file and set up the url
let showPath = episode?.showPath
let imageFile = episode?.imageFile
let url = "https://examplesite.com/ios/images/" + showPath! + "/slider/" + imageFile! + ".jpg"
// Show the image
episodeImage.sd_setImage(with: URL(string: url))
}
}
which in turn calls getFeed() since the user has been verified as subscribed
func getFeed(url: String) {
Alamofire.SessionManager.default
.requestWithoutCache(url, method: .get).validate().responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
self.buildPlaylistSources(json: json)
case .failure(let error):
print(error)
}
}
}
This will call the function to build the playlist for the JW Player,
func buildPlaylistSources(json: JSON) {
playlistSources = [[String:String]]()
if let playlistItems = json["playlist"].array {
for item in playlistItems {
if let sources = item["sources"].array {
// For each source
var tempSource = [String:String]()
for source in sources {
if let sourceType = source["type"].string {
if sourceType.hasPrefix("video"){
let key = source["label"].stringValue
let value = source["file"].stringValue
tempSource[key] = value
}
}
}
playlistSources.append(tempSource)
}
}
createPlayer()
}
}
and subsequently set up the player and add it to the screen
func createPlayer() {
let config: JWConfig = JWConfig()
let showPath = episode?.showPath
let imageFile = episode?.imageFile
let videoImage = "https://examplesite.com/ios/images/" + showPath! + "/slider/" + imageFile! + ".jpg"
for playlistSource in playlistSources {
let item = JWPlaylistItem()
var sourceArray = [JWSource]()
for (key, value) in playlistSource {
sourceArray.append(JWSource(file: value, label: key))
}
item.sources = sourceArray
item.image = videoImage
playlist.append(item)
}
config.playlist = playlist
config.controls = true
player = JWPlayerController(config: config)
player.delegate = self
let frame: CGRect = containerView.bounds
self.player.view.frame = frame
self.player.view.autoresizingMask = [UIViewAutoresizing.flexibleBottomMargin, UIViewAutoresizing.flexibleHeight, UIViewAutoresizing.flexibleLeftMargin, UIViewAutoresizing.flexibleRightMargin, UIViewAutoresizing.flexibleTopMargin, UIViewAutoresizing.flexibleWidth]
self.player.forceFullScreenOnLandscape = true
self.player.forceLandscapeOnFullScreen = true
containerView.addSubview(player.view)
print("jwplayer")
}
Even though the code is running all the way to the end, and the initial image is hidden, the player is never loaded. If however I then navigate to a different section and then return to the Episode view, the video loads without issues. Also, if the user runs the app while having previously logged in, the videos load fine. It's just upon returning from the modal login screen that the video player won't load.
I checked the output screen and got the following:
2018-04-11 08:52:54.515047-0500 MyAppIOS[8506:1047732] [MediaRemote] [AVOutputContext] WARNING: AVF context unavailable for +[MRAVOutputContext sharedAudioPresentationContext]_block_invoke
2018-04-11 08:52:54.515273-0500 MyAppIOS[8506:1047732] [MediaRemote] [AVOutputContext] WARNING: AVF context unavailable for +[MRAVOutputContext createOutputContextWithUniqueIdentifier:]
2018-04-11 08:52:57.476625-0500 MyAppIOS[8506:1050590] [0x7f95a686f000] Decoding failed with error code -1
2018-04-11 08:52:57.476904-0500 MyAppIOS[8506:1050590] [0x7f95a686f000] Decoding: C0 0x02800168 0x0000354A 0x11111100 0x00000000 16384
2018-04-11 08:52:57.477102-0500 MyAppIOS[8506:1050590] [0x7f95a686f000] Options: 640x360 [FFFFFFFF,FFFFFFFF] 00024060
As answered by NSGangster in the comments:
Upon a going to the VC the first time (not logged in) you set containerView.isHidden = true. you Probably should set this to false somewhere if you want it to show back up. Which is why I assume it works if you reload the VC.

Media Player filtering and prepending album plays album, a random song, and then the album again

I am trying to make a music player and currently I am stuck on filtering and prepending an album to the playlist.
What I am trying to do is play music on shuffle and then when a user taps a button continue playing songs only from the album currently playing, when it is finished I want to know its finished so I can change UI elements and then go on to play any and all music from the library.
However, what happens is it will play the rest of the songs from the album THEN when it has exhausted all the songs from that album will play a random song then after that one random song will go back to that album ..go through its entirety and then play random songs. Once in a blue moon after it finishes the album it will just play random songs.
In a singleton I have
func getAllSongs(completion: #escaping (_ songs: [MPMediaItem]?) -> Void) {
MPMediaLibrary.requestAuthorization { (status) in
if status == .authorized {
let query = MPMediaQuery()
let mediaTypeMusic = MPMediaType.music
let audioFilter = MPMediaPropertyPredicate(value: mediaTypeMusic.rawValue, forProperty: MPMediaItemPropertyMediaType, comparisonType: MPMediaPredicateComparison.equalTo)
query.addFilterPredicate(audioFilter)
let songs = query.items
completion(songs)
} else {
completion(nil)
}
}
}
func getSongsWithCurrentAlbumFor(item: MPMediaItem) -> MPMediaQuery {
let albumFilter = MPMediaPropertyPredicate(value: item.albumTitle, forProperty: MPMediaItemPropertyAlbumTitle, comparisonType: MPMediaPredicateComparison.equalTo)
let predicates: Set<MPMediaPropertyPredicate> = [albumFilter]
let query = MPMediaQuery(filterPredicates: predicates)
query.addFilterPredicate(albumFilter)
return query
}
In my VC to set up the audio I use
let mediaPlayer = MPMusicPlayerApplicationController.applicationMusicPlayer
func setUpAudioPlayerAndGetSongsShuffled() {
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient)
try? AVAudioSession.sharedInstance().setActive(true)
MBProgressHUD.showAdded(to: view, animated: true)
MediaManager.shared.getAllSongs { (songs) in
guard let theSongs = songs else {
return
}
self.newSongs = theSongs.filter({ (item) -> Bool in
return !self.playedSongs.contains(item)
})
self.mediaPlayer.setQueue(with: MPMediaItemCollection(items: self.newSongs))
self.mediaPlayer.shuffleMode = .songs
self.mediaPlayer.repeatMode = .none
self.mediaPlayer.prepareToPlay(completionHandler: { (error) in
DispatchQueue.main.async {
MBProgressHUD.hide(for: self.view, animated: true)
}
})
}
}
When the user taps the button to continue playing songs only from that album I use
if let nowPlaying = mediaPlayer.nowPlayingItem {
let albumQuery = MediaManager.shared.getSongsWithCurrentAlbumFor(item: nowPlaying)
print("\(albumQuery)")
let descriptor = MPMusicPlayerMediaItemQueueDescriptor(query: albumQuery)
mediaPlayer.prepend(descriptor)
}
Upon rereading the documentation I notice it says that I should change to
let mediaPlayer = MPMusicPlayerApplicationController. applicationQueuePlayer
I cannot figure out how to know when an album has been exhausted and then continue playing the rest of the music library.
it would be great to know if the album does not have any other items so that the user could not press the button to play more from the album
I discovered my mistakes. I was putting my logic in the wrong place
In my singleton I added
func hasPlayedAllSongsFromAlbumFor(song: MPMediaItem) -> Bool {
if let allSongsInAlbum = getSongsWithCurrentAlbumFor(item: song).items {
return lockedSongsContains(songs: allSongsInAlbum)
}
return true
}
func lockedSongsContains(songs: [MPMediaItem]) -> Bool {
for aSong in songs {
if !lockedSongs.contains(aSong) {
return false
}
}
return true
}
I needed to use a Notification
I created a func that has a notification in the ViewDidLoad called songChanged
In the songChanged func I have
if albumIsLocked && MediaManager.shared.hasPlayedAllSongsFromAlbumFor(song: nowPlaying) {
albumLockButtonTapped(albumLockIconButton)
MediaManager.shared.lockedSongs.removeAll()
}
On the album lock button I now use
if let nowPlaying = mediaPlayer.nowPlayingItem {
if sender.isSelected {
sender.isSelected = false
albumIsLocked = false
let lockRemovedQuery = MediaManager.shared.removeAlbumLockFor(item: nowPlaying)
let lockRemovedDescriptor = MPMusicPlayerMediaItemQueueDescriptor(query: lockRemovedQuery)
mediaPlayer.prepend(lockRemovedDescriptor)
mediaPlayer.prepareToPlay()
} else {
sender.isSelected = true
albumIsLocked = true
let albumQuery = MediaManager.shared.getSongsWithCurrentAlbumFor(item: nowPlaying)
let albumDescriptor = MPMusicPlayerMediaItemQueueDescriptor(query: albumQuery)
mediaPlayer.prepend(albumDescriptor)
mediaPlayer.prepareToPlay()
}
}

iOS Adjust Pitch Whilst Playing via AVAudioUnitTimePitch

I’m trying to get some audio to be able to have the pitch adjusted whilst playing. I’m very new to Swift and iOS, but my initial attempt was to just change timePitchNode.pitch whilst it was playing; however, it wouldn’t update whilst playing. My current attempt is to reset audioEngine, and have it just resume from where it was playing (below). How do I determine where the audio currently is, and how do I get it to resume from there?
var audioFile: AVAudioFile?
var audioEngine: AVAudioEngine?
var audioPlayerNode: AVAudioPlayerNode?
var pitch: Int = 1 {
didSet {
playResumeAudio()
}
}
…
func playResumeAudio() {
var currentTime: AVAudioTime? = nil
if audioPlayerNode != nil {
let nodeTime = audioPlayerNode!.lastRenderTime!
currentTime = audioPlayerNode!.playerTimeForNodeTime(nodeTime)
}
if audioEngine != nil {
audioEngine!.stop()
audioEngine!.reset()
}
audioEngine = AVAudioEngine()
audioPlayerNode = AVAudioPlayerNode()
audioEngine!.attachNode(audioPlayerNode!)
let timePitchNode = AVAudioUnitTimePitch()
timePitchNode.pitch = Float(pitch * 100)
timePitchNode.rate = rate
audioEngine!.attachNode(timePitchNode)
audioEngine!.connect(audioPlayerNode!, to: timePitchNode, format: nil)
audioEngine!.connect(timePitchNode, to: audioEngine!.outputNode, format: nil)
audioPlayerNode!.scheduleFile(audioFile!, atTime: nil, completionHandler: nil)
let _ = try? audioEngine?.start()
audioPlayerNode!.playAtTime(currentTime)
}
I was being dumb apparently. You can modify the pitch during playback, and it does update. No need to reset any audio, just mutate the node as it’s playing, and it’ll work.

How to show AVplayer current play duration in UISlider

I am using a custom UISlider to implement the scrubbing while playing a video in AVPlayer. Trying to figure out the best way to show the current play time and the remaining duration on the respective ends of the UISlider as it usually is shown in MPMoviePlayerController.
Any help is appreciated.
Swift 4.x
let player = AVPlayer()
player.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: 1), queue: .main, using: { time in
if let duration = player.currentItem?.duration {
let duration = CMTimeGetSeconds(duration), time = CMTimeGetSeconds(time)
let progress = (time/duration)
if progress > targetProgress {
print(progress)
//Update slider value
}
}
})
or
extension AVPlayer {
func addProgressObserver(action:#escaping ((Double) -> Void)) -> Any {
return self.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: 1), queue: .main, using: { time in
if let duration = self.currentItem?.duration {
let duration = CMTimeGetSeconds(duration), time = CMTimeGetSeconds(time)
let progress = (time/duration)
action(progress)
}
})
}
}
Use
let player = AVPlayer()
player.addProgressObserver { progress in
//Update slider value
}
Put a UILabel at each end. Update them using -[AVPlayer addPeriodicTimeObserverForInterval:queue:usingBlock:]. Compute the time remaining using -[AVPlayer currentTime] and -[AVPlayerItem duration].
[slider setMaximumValue:[AVPlayerItem duration]];
//use NSTimer, run repeat function
-(void)updateValueSlide{
[slider setValue:[AVPlayerItem duration]];
}

Resources