Ios AVPlayer - Wait for buffering [duplicate] - ios

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

Related

Is there a better way to watch for a key-value change?

I am wondering if the following is really the best way to know when a video is ready to play: Currently what I do is if currentMedia.playerQueue?.status.rawValue == 1 { I play, else (video has not loaded yet) I will show loading icon and then do the following to observe for when the video is ready to play:
I create a AVPlayer using the same video url and then when its ready I will call the playCurentMedia() method to play vid.
currentMedia.avPlayer = AVPlayer(url: currentMedia.videoURL!)
currentMedia.avPlayer!.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: &P2SheetViewController.playerStatusContext)
Does this stragtegy ahve any flaws? Or is it a good solution?
How can I then observe for a change so I can show the video when it's ready?
Currently, I am only able to set up a AVPlayer alongside the queue so that I can observe for its change...
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("observe KVO")
// Only handle observations for the playerItemContext
guard context == &P2SheetViewController.playerStatusContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
if keyPath == #keyPath(AVPlayer.status) {
let status: AVPlayer.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
//Switch over status value
switch status {
case .readyToPlay:
print("READY TO PLAY")
GlobalSharedData.shared.videoAllSetToGoMedia1 = true
if GlobalSharedData.shared.p2Media1VideoWasNotReadyWhenPressedView {
baseVC.playVideoControlForP2()
}
break
// Player item is ready to play.
case .failed:
print(".UKNOWN")
break
// Player item failed. See error.
case .unknown:
print(".UKNOWN")
break
// Player item is not yet ready.
}
}
}
According to the official AVPlayer documentation, there are two approaches you can use to observe a player’s state:
General State Observations: You can use Key-value observing (KVO) to observe state changes to many of the player’s dynamic properties, such as its currentItem or its playback rate. You should register and unregister for KVO change notifications on the main thread. This avoids the possibility of receiving a partial notification when making a change on another thread. AVFoundation invokes observeValue(forKeyPath:of:change:context:) on the main thread, even when making the change operation on another thread.
Timed State Observations: KVO works well for general state observations, but isn’t intended for observing continuously changing state like the player’s time. AVPlayer provides two methods to observe time changes:
addPeriodicTimeObserver(forInterval:queue:using:)
addBoundaryTimeObserver(forTimes:queue:using:)
These methods let you observe time changes either periodically or by boundary, respectively. As changes occur, invoke the callback block or closure you supply to these methods to give you the opportunity to take some action such as updating the state of your player’s user interface.
Also, see AVPlayer status property note:
The player’s status does not indicate its readiness to play a specific player item. You should instead use the status property of AVPlayerItem to make that determination.
So, if you want to know when the specific video is loaded and ready to play you should create AVPlayerItem and observe its status property. When the player item’s media has been loaded and is ready for use, its status will change to AVPlayerItem.Status.readyToPlay.

Why is AVPlayer Boundary Time Observer not working?

I am trying to observe a time in the timeline of my AVPlayer.
I tried this on the main queue; which did not work. I then switched to a background queue, as advised from this stack overflow post; which did not with either. Looking for a working solution or an explanation as to why this isn't working.
//add boundary time notification to global queue
avLayer.player!.addBoundaryTimeObserver(forTimes: [NSValue(time: avLayer.player!.currentItem!.duration)], queue: DispatchQueue.main){
self.avLayer.player!.pause()
}
//add boundary time notification to background queue
avLayer.player!.addBoundaryTimeObserver(forTimes: [NSValue(time: avLayer.player!.currentItem!.duration)], queue: DispatchQueue.global(qos: .userInteractive)){
self.avLayer.player!.pause()
}
Update: After retaining a strong reference to the return value of the observer, I set a breakpoint in the callback. It is still not working.
//add boundary time notification
boundaryTimeObserver = avLayer.player!.addBoundaryTimeObserver(forTimes: [NSValue(time: avLayer.player!.currentItem!.duration)], queue: DispatchQueue.main){
self.avLayer.player!.pause()
}
2019 simple example ..
var player = AVPlayer()
var token: Any?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let u = "https... "
let playerItem = AVPlayerItem(url: URL(string: u)!)
player = AVPlayer(playerItem: playerItem)
player.play()
token = player.addBoundaryTimeObserver(
forTimes: [0.5 as NSValue],
queue: DispatchQueue.main) { [weak self] in
self?.spinner.stopAnimating()
print("The audio is in fact beginning about now...")
}
}
Works perfectly.
Important .. it won't find "0"
Use a small value to find the "beginning" as a quick solution.
There may be two problems:
As the documentation for addBoundaryTimeObserver states:
You must maintain a strong reference to the returned value as long as you want the time observer to be invoked by the player
As your initial code does not keep a reference to the returned internal opaque time observer, the observer probably is released immediately and thus is never called.
Make sure the time you register for observing actually has the correct value:
playerItem.duration may be indefinite (see documentation of this property)
even the duration of the playerItem's asset may be unknown, or an in-precise estimation, depending on the type of the asset and loading state (again, see documentation of AVAsset.duration on this).
As a consequence, the time you register for observing may never be reached (note that the time can easily be checked by inserting a CMTimeShow(duration))
Approaches to resolve this:
if you just want to stop the player when the playerItem's end is reached, setting player.actionAtItemEnd to pause may be sufficient
if you need to execute some custom logic when the item's end is reached, register an observer for AVPlayerItemDidPlayToEndTime notifications with the playerItem as object. This mechanism is independent from possibly in-precise durations and so hopefully more reliable

How to get animation to work at exact points during playback of a music file?

Question:
In Swift code, apart from using an NSTimer, how can I get animations
to start at exact points during playback of a music file played using AVFoundation?
Background
I have a method that plays a music file using AVFoundation (below). I also have UIView animations that I want to start at exact points during the music file being played.
One way I could achieve this is using an NSTimer, but that has the potential to get out of sync or not be exact enough.
Is there a method that I can tap into AVFoundation accessing the music file's time elapsed (time counter), so when certain points during the music playback arrive, animations start?
Is there an event / notification that AVFoundation triggers that gives a constant stream of time elapsed since the music file has started playing?
For example
At 0:52.50 (52 seconds and 1/2), call startAnimation1(), at 1:20.75 (1 minute, 20 seconds and 3/4), call startAnimation2(), and so on?
switch musicPlayingTimeElapsed {
case 0:52.50:
startAnimation1()
case 1:20.75:
startAnimation2()
default:
()
}
Playing music using AVFoundation
import AVFoundation
var myMusic : AVAudioPlayer?
func playMusic() {
if let musicFile = self.setupAudioPlayerWithFile("fileName", type:"mp3") {
self.myMusic = musicFile
}
myMusic?.play()
}
func setupAudioPlayerWithFile(file:NSString, type:NSString) -> AVAudioPlayer? {
let path = NSBundle.mainBundle().pathForResource(file as String, ofType: type as String)
let url = NSURL.fileURLWithPath(path!)
var audioPlayer:AVAudioPlayer?
do {
try audioPlayer = AVAudioPlayer(contentsOfURL: url)
} catch {
print("AVAudioPlayer not available")
}
return audioPlayer
}
If you use AVPlayer instead of AVAudioPlayer, you can use the (TBH slightly awkward) addBoundaryTimeObserverForTimes method:
let times = [
NSValue(CMTime:CMTimeMake(...)),
NSValue(CMTime:CMTimeMake(...)),
NSValue(CMTime:CMTimeMake(...)),
// etc
];
var observer: AnyObject? = nil // instance variable
self.observer = self.player.addBoundaryTimeObserverForTimes(times, queue: nil) {
switch self.player.currentTime() {
case 0:52.50:
startAnimation1()
case 1:20.75:
startAnimation2()
default:
break
}
}
// call this to stop observer
self.player.removeTimeObserver(self.observer)
The way I solve this is to divide the music up into separate segments beforehand. I then use one of two approaches:
I play the segments one at a time, each in its own audio player. The audio player's delegate is notified when a segment finishes, and so starting the next segment — along with accompanying action — is up to me.
Alternatively, I queue up all the segments onto an AVQueuePlayer. I then use KVO on the queue player's currentItem. Thus, I am notified exactly when we move to a new segment.
You might try using Key Value Observing to observe the duration property of your sound as it plays. When the duration reaches your time thresholds you'd trigger each animation. You'd need to make the time thresholds match times >= the trigger time, since you will likely not get a perfect match with your desired time.
I don't know how well that would work however. First, I'm not sure if the sound player's duration is KVO-compliant.
Next, KVO is somewhat resource-intensive, and if your KVO listener gets called thousands of times a second it might bog things down. It would at least be worth a try.

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.

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

Resources