Achieve smooth video scrubbing with AVPlayer - ios

I am trying to achieve smooth video scrubbing with AVPlayer through UISlider I have searched and it seems Apple has a Technical Q&A and explained how to achieve this, but my problem is how should I use this method and change player current time with a UISlider:
stopPlayingAndSeekSmoothlyToTime(newChaseTime:CMTime)
Here is my code :
//Play Intro Movie
let videoURL = Bundle.main.url(forResource: "intro", withExtension: "mp4")
player = AVPlayer(url:videoURL!)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.frame
view.layer.addSublayer(playerLayer)
//videoPlayer.play()
view.addSubview(slider)
slider.maximumValue = 0
slider.maximumValue = Float(CMTimeGetSeconds((player.currentItem?.asset.duration)!))
Here is Apple sample code :
func stopPlayingAndSeekSmoothlyToTime(newChaseTime:CMTime)
{
player.pause()
if CMTimeCompare(newChaseTime, chaseTime) != 0
{
chaseTime = newChaseTime;
if !isSeekInProgress
{
trySeekToChaseTime()
}
}
}
func trySeekToChaseTime()
{
if playerCurrentItemStatus == .unknown
{
// wait until item becomes ready (KVO player.currentItem.status)
}
else if playerCurrentItemStatus == .readyToPlay
{
actuallySeekToTime()
}
}
func actuallySeekToTime()
{
isSeekInProgress = true
let seekTimeInProgress = chaseTime
player.seek(to: seekTimeInProgress, toleranceBefore: kCMTimeZero,
toleranceAfter: kCMTimeZero, completionHandler:
{ (isFinished:Bool) -> Void in
if CMTimeCompare(seekTimeInProgress, self.chaseTime) == 0
{
self.isSeekInProgress = false
}
else
{
self.trySeekToChaseTime()
}
})
}

Although I'm not using the same method as you do which is stopPlayingAndSeekSmoothlyToTime, I thought I should help you with the seeking action of the player.
func sliderValueChanged() {
var timeToSeek = player.currentItem?.asset.duration.seconds
timeToSeek = timeToSeek * Double(slider.value)
player.seek(to: CMTimeMake(Int64(timeToSeek), 1))
}
Also you should set the slider.maximumValue to 1. Hope this helps.
Note: Please don't forget to handle currentItem optional value. If it is nil you should set the value 0 for timeToSeek variable.

On slider value change event, just call the
stopPlayingAndSeekSmoothlyToTime(CMTime.init(seconds: (player.currentItem?.asset.duration.seconds)!* slider.value, preferredTimescale: 1000))
The Apples sample code will change the player current time. You can also adjust toleranceBefore and toleranceAfter if you scrub the slider really fast.

try to add selectors to your slider like this at the begining:
playerView.timeSlider.addTarget(self, action: #selector(PlayerViewController.timeSliderBeganTracking(_:)), for:.touchDown) // moved
playerView.timeSlider.addTarget(self, action: #selector(PlayerViewController.timeSliderEndedTracking(_:)), for: .touchUpInside)
playerView.timeSlider.addTarget(self, action: #selector(PlayerViewController.timeSliderEndedTracking(_:)), for: .touchUpOutside )
then in timeSliderBeganTracking set isSeekInProgress to true and pause your player and invalidate timers and remove observers if you have any
then in timeSliderEndedTracking
seek to current time like this :
let currentSeconds = Float64(self.playerView.timeSlider.value)
container.player.seek(to: CMTimeMakeWithSeconds(currentSeconds, 100))
then add the observers and timers back and at the end set isSeekInProgress to false
HERE you can find a complete sample of creating a custom videoPlayer

I know this is an old problem, I encountered it too. The solution can help someone now. Apple sample make slider super smooth and pretty. To make the sample work:
private static var context = 1
func playVideo() {
...
player.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.status), options: [.new, .initial], context: &VideoViewController.context)
...
}
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return }
guard context == &VideoViewController.context else { return }
switch keyPath {
case #keyPath(AVPlayer.currentItem.status):
if let newStatus = change.map({ $0[.newKey] as? NSNumber }),
let parsedNewStatus = newStatus.map({ AVPlayerItem.Status(rawValue: $0.intValue) ?? .unknown}) {
playerCurrentItemStatus = parsedNewStatus
} else {
playerCurrentItemStatus = .unknown
}
default: break
}
}
Observe and update playerCurrentItemStatus

Related

AVPlayerViewController took too much time to play video/audio with automaticallyWaitsToMinimizeStalling

I am using AVPlayer, in Player I want to play video from the Server. I am doing like the code below. Problem I am facing is I set the
automaticallyWaitsToMinimizeStalling = true
According to Documentation
A Boolean value that indicates whether the player should automatically delay playback in order to minimize stalling.
but it took too much time to load the video/audio, for 8minutes video it took almost 2 to 3 minutes wait to play. If during this time (wait time of 2 to 3 minutes), user pause the video and play again, then video will play without any delay. This unnecessary wait should not be happened.
Can anyone guide me how to decrease wait of stalling? I can not use this answer
player.automaticallyWaitsToMinimizeStalling = false
because due to set this value false, my player stops again and again, and user have to play this manually, so this thing is very bad.
// MARK: - Outlets
#IBOutlet weak var audioView: UIView!
// MARK: - Variables
var player: AVPlayer = AVPlayer()
let playerController = AVPlayerViewController()
var obs = Set<NSKeyValueObservation>()
// MARK: - Helper Method
private func settingForAudioPlayer() {
guard let lesson = viewModal.lesson else {
print("lesson not found")
return
}
var path = "\(Server.audioVideoBasurl + (lesson.videoPath ?? ""))"
path = path.replacingOccurrences(of: " ", with: "%20")
print("path:\(path)")
guard let url = URL(string: path) else {
print("Path not converted to url")
return
}
self.player = AVPlayer(url: url)
self.player.automaticallyWaitsToMinimizeStalling = true
self.player.playImmediately(atRate: 1.0)
self.playerController.player = self.player
DispatchQueue.main.async {
self.playerController.view.clipsToBounds = true
self.playerController.view.removeFromSuperview()
self.playerController.delegate = self
self.showSpinner(onView: self.audioView, identifier: "audioView", title: "")
self.audioView.addSubview(self.playerController.view)
self.audioView.layoutIfNeeded() // Previously we were playing only audio but after some time we decided to add videos also so thats why that view name is audioView Don’t get confuse with this view name
self.playerController.view.frame.size.width = self.audioView.frame.width
self.playerController.view.frame.size.height = self.audioView.frame.height
self.playerController.view.backgroundColor = .clear
self.playerController.videoGravity = AVLayerVideoGravity.resizeAspectFill
var ob : NSKeyValueObservation!
ob = self.playerController.observe(\.isReadyForDisplay, options: [.initial, .new]) { vc, ch in
guard let ok = ch.newValue, ok else {return}
self.obs.remove(ob)
DispatchQueue.main.async {
print("finishing")
self.removeSpinner(identifier: "audioView") // This is my Custom Method
vc.view.isHidden = false // The Idea of KVO observer add got from the Internet
}
}
self.obs.insert(ob)
let iv = self.audioBackgroundImageView ?? UIImageView()
let v = self.playerController.contentOverlayView!
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.bottomAnchor.constraint(equalTo:v.bottomAnchor),
iv.topAnchor.constraint(equalTo:v.topAnchor),
iv.leadingAnchor.constraint(equalTo:v.leadingAnchor),
iv.trailingAnchor.constraint(equalTo:v.trailingAnchor),
])
NSLayoutConstraint.activate([
v.bottomAnchor.constraint(equalTo:self.playerController.view.bottomAnchor),
v.topAnchor.constraint(equalTo:self.playerController.view.topAnchor),
v.leadingAnchor.constraint(equalTo:self.playerController.view.leadingAnchor),
v.trailingAnchor.constraint(equalTo:self.playerController.view.trailingAnchor),
])
self.view.layoutIfNeeded()
}
}
in Above peace of code I have video url (Path), I passed it to the AVPlayer,
Player pass to AVPlayerController,
add observer to the playerController to check that AVPlayerController is ready for Display, then remove observer.
After that I am only settings the constraint.
Kindly guide me how to decrease the waits on swift, on Android side video/Audio plays with in seconds.
It can be duplicate of this but my scenario is different and the solution here did not work for me. Kindly let me know in-case you need more information to help me.
If you don't want to use automaticallyWaitsToMinimizeStalling = true you can also observe player buffer and decide whether you start playing video. Here are some steps how to do that :
Create observer variable in the class where you intend to handle player:
var playbackBufferEmptyObserver: NSKeyValueObservation?
var playbackBufferFullObserver: NSKeyValueObservation?
var playbackLikelyToKeepUpObserver: NSKeyValueObservation?
Instantiate AVPlayer with AVPlayerItem instead of URL likewise:
let playerItem = AVPlayerItem(url: url)
let player = AVPlayer(playerItem: playerItem)
Create observers and assign them to variables:
playbackBufferEmptyObserver = self.playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .initial ], changeHandler: { [weak self] (player, bufferEmpty) in
if let self = self {
DispatchQueue.main.async {
if bufferEmpty.newValue == true {
// handle showing loading, player not playing
}
}
}
})
playbackBufferFullObserver = self.playerItem.observe(\.isPlaybackBufferFull, options: [.new, .initial], changeHandler: {[weak self] (player, bufferFull) in
if let self = self {
DispatchQueue.main.async {
if bufferFull.newValue == true {
//handle when player buffer is full (e.g. hide loading) start player
}
}
}
})
playbackLikelyToKeepUpObserver = self.playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .initial], changeHandler: { [weak self] (player, _) in
if let self = self {
if ((self.playerItem?.status ?? AVPlayerItem.Status.unknown)! == .readyToPlay) {
// handle that player is ready to play (e.g. hide loading indicator, start player)
} else {
// player is not ready to play yet
}
}
})

Swift -AVPlayer' KVO in cell's parent vc causing Xcode to freeze

I have a cell that takes up the entire screen so there is only 1 visible cell at a time. Inside the cell I have an AVPlayer. Inside the cell's parent vc I have a KVO observer that listens to the "timeControlStatus". When the player stops playing I call a function stopVideo() inside the cell to stop the player and either show a replay button or a play button etc.
3 problems:
1- When the video stops/reaches end if I don't use DispatchQueue inside the KVO the app crashes when the cell's function is called.
2- When the video stops and I do use DispatchQueue inside the KVO it's observer keeps observing and Xcode freezes (no crash). There is a print statement inside the KVO that prints indefinitely and that's why Xcode freezes. The only way to stop it is to kill Xcode otherwise the beachball of death keeps spinning.
3- In the KVO I tried to use a notification to send to the cell in place of calling the cell.stopVideo() function but the function inside the cell never runs.
How can i fix this issue?
It should be noted that outside of the KVO not working everything else works fine. I have a periodic time observer that runs perfectly for every cell whenever I scroll, the videos load fine, and when I press the cell stop/play the video it all works fine.
cell:
protocol MyCellDelegate: class {
func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?)
}
var player: AVPlayer?
var indexPath: IndexPath?
var playerItem: AVPlayerItem? {
didSet {
// add playerItem to player
delegate?.sendBackPlayerAndIndexPath(player, indexPath)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
player = AVPlayer()
// set everything else relating to the player
}
// both get initialized in cellForItem
var delegate: MyCellDelegate?
var myModel: MyModel? {
didSet {
let url = URL(string: myModel!.videUrlStr!)
asset = AVAsset(url: url)
playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["playable"])
}
}
// tried using this with NotificationCenter but it didn't trigger from parent vc
#objc public func playVideo() {
if !player?.isPlaying {
player?.play()
}
// depending on certain conditions show a mute button, etc
}
// tried using this with NotificationCenter but it didn't trigger from parent vc
#objc public func stopVideo() {
player?.pause()
// depending on certain conditions show a reload button or a play button etc
}
parent vc
MyVC: ViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var player: AVPlayer?
var currentIndexPath: IndexPath?
var isObserving = false
func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?) {
if isObserving {
self.player?.removeObserver(self, forKeyPath: "status", context: nil)
self.player?.removeObserver(self, forKeyPath: "timeControlStatus", context: nil)
}
guard let p = player, let i = currentIndexPath else { return }
self.player = p
self.currentIndexPath = i
isObserving = true
self.player?.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil)
self.player?.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}
// If I don't use DispatchQueue below the app crashes
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
}
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
} else {
print("3. Player is Not Playing *** ONCE STOPPED THIS PRINTS FOREVER and Xcode freezes but doesn't crash.\n")
DispatchQueue.main.async { [weak self] in
self?.stopVideoInCell()
}
}
}
}
}
func playVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
cell.playVideo()
// also tried sending a NotificationCenter message to the cell but it didn't trigger
}
func stopVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
cell.stopVideo()
// also tried sending a NotificationCenter message to the cell but it didn't trigger
}
In the comments #matt asked for the crash log (only occurs when not using DispatchQueue inside the KVO). I have zombies enabled and it didn't give me any info. The crash happens instantaneously and then just goes blank. It doesn't give me any info. I had to quickly take a screen shot just to get the pic otherwise it disappears right after the crash.
Inside the KVO, the 3rd one that prints forever, I removed the DispatchQueue and just added the stopVideoInCell() function. When the function calls the cell's stopVideo() function, player?.pause briefly gets a EXC_BAD_ACCESS (code=2, address=0x16b01ff0) crash.
The app then terminates. Inside the console the only thing that is printed is:
Message from debugger: The LLDB RPC server has crashed. The crash log
is located in ~/Library/Logs/DiagnosticReports and has a prefix
'lldb-rpc-server'. Please file a bug and attach the most recent crash
log
When I go to terminal and see what prints out the only thing I get is a bunch of lldb-rpc-server_2020-06-14-155514_myMacName.crash statements from all the days I ran into this crash.
When using DispatchQueue this doesn't occur and the video does stop but of course that print statement inside the KVO runs forever and Xcode freezes.
The problem is that, in your property observer, you are making a change in the property that you are observing. That is a vicious circle, an infinite recursion; Xcode displays this by freezing your app until ultimately you crash with (oh, the irony) a stack overflow.
Let's take a simpler, self-contained example. We have a UISwitch in the interface. It's On. If the user switches it Off, we want to detect that and switch it back to On. The example is a silly way to do this, but it perfectly illustrates the issue you are facing:
class ViewController: UIViewController {
#IBOutlet weak var theSwitch: UISwitch!
class SwitchHelper: NSObject {
#objc dynamic var switchState : Bool = true
}
let switchHelper = SwitchHelper()
var observer: NSKeyValueObservation!
override func viewDidLoad() {
super.viewDidLoad()
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
}
}
#IBAction func doSwitch(_ sender: Any) {
self.switchHelper.switchState = (sender as! UISwitch).isOn
}
}
What's going to happen? The user turns the switch Off. We are observing that, as switchState; in response, we switch the switch back to On and call sendActions. And sendActions changes switchState. But we are still in the middle of the code that observes switchState! So we do it again and it happens again. And again and it happens again. Infinite loop...
How would you get out of this? You need to break the recursion somehow. I can think of two obvious ways. One is to think to yourself, "Well, I only care about a switch from On to Off. I don't care about the other way." Assuming that's true, you can solve the problem with a simple if, rather like the solution you elected to use:
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
if let val = change.newValue, !val {
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
}
}
A more elaborate solution, which I like to use sometimes, is to stop observing when the observer is triggered, make whatever the change is, and then start observing again. You have to plan ahead a little to implement that, but it's sometimes worth it:
var observer: NSKeyValueObservation!
func startObserving() {
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
self.observer?.invalidate()
self.observer = nil
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
self.startObserving()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.startObserving()
}
That looks recursive, because startObserving calls itself, but it isn't really, because what it does when it is called is to configure the observation; the code in the inner curly braces doesn't run until we get an observed change.
(In real life, I would probably make the NSKeyValueObservation a local variable in that configuration. But that's just an additional bit of elegance, not essential to the point of the example.)
I resolved this using a Boolean. It's not the most elegant answer but it works. If someone can come up with a better answer I'll accept it. It doesn't make sense what's going because of what I put under more:
answer:
var isPlayerStopped = false
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
}
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
} else {
if isPlayerStopped { return }
print("3. Player is Not Playing *** NOW THIS ONLY PRINTS ONCE.\n")
DispatchQueue.main.async { [weak self] in
self?.stopVideoInCell()
}
}
}
}
}
func playVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
isPlayerStopped = false
cell.playVideo()
}
func stopVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
isPlayerStopped = true
cell.stopVideo()
}
more:
If I completely remove the DispatchQueues and the functions inside of them and use just print statements, the print statement that prints indefinitely print("3. Player is Not Playing... \n") only prints twice, it no longer prints indefinitely so I don't know what's going on with this.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
print("1. Player is Playing\n")
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
print("2. Player is Playing\n")
} else {
print("3. Player is Not Playing... \n")
}
}
}
}

How to tell when AVPlayer has been played for three seconds Swift

I have an application that contains videos that play automatically in an UIImageView in a UITableView when the cell is visible, and all I am trying to do is allow the application to know when the video has been played for three seconds. I wrote this code.
class PostCell: UITableViewCell {
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
var post: Post? {
didSet {
updateView()
}
}
func updateView() {
self.viewcount()
if let videoUrlString = post?.videoUrl, let videoUrl = URL(string: videoUrlString) {
player = AVPlayer(url: videoUrl)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = postImageView.frame
playerLayer?.frame.size.width = postImageView.frame.size.width
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.contentView.layer.addSublayer(playerLayer!)
player?.play()
}
func viewcount() {
if let currentitem = player?.currentItem {
if currentitem.currentTime() == CMTimeMake(3, 1) {
print ("VIDEO PLAYED FOR THREE SECONDS")
}
}
}
}
but it is not printing out my message once the video starts playing. I have searched the web for help but couldn't find anything on this subject. So could anyone please help with my issue and tell me what I am doing wrong ?
You are searching for observer of player here is how you can check and track the current position of AVPlayer
Here is function that is adding observer to cell
private func addObserversForVideoPlayer(cell:CustomCell) {
let observer = cell.player?.addPeriodicTimeObserver(forInterval: CMTime.init(seconds: 1, preferredTimescale: 1), queue: .main, using: {[weak self,weak cell] (time) in
guard let cell = cell else {return}
if cell.player?.currentItem?.status == .readyToPlay {
// print("Inside Will DISPLAY\(cell.video.currentTime)")
let timeDuration : Float64 = CMTimeGetSeconds((cell.player?.currentItem?.asset.duration)!)
cell.lblDuration.text = self?.getDurationFromTime(time: timeDuration)
let currentTime : Float64 = CMTimeGetSeconds((cell.player?.currentTime())!)
cell.lblStart.text = self?.getDurationFromTime(time: currentTime)
cell.slider.maximumValue = Float(timeDuration.rounded())
cell.slider.value = Float(currentTime.rounded())
}
})
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: cell.player?.currentItem, queue: .main, using: {[weak cell,weak self] (notification) in
if cell?.player != nil {
cell?.player?.seek(to: kCMTimeZero)
cell?.player?.play()
}
})
}
so that addPeriodicTimeObserver will notify you when the player start playing.
And NSNotification.Name.AVPlayerItemDidPlayToEndTime will notify you when your AVPlayer stops.
Note1: If your cell.player?.currentItem is nil while you are adding AVPlayerItemDidPlayToEndTime it will be cause bug see this One AVPlayer's AVPlayerItemDidPlayToEndTime action executed for all Currently playing videos , If . you don't need it don't add it :)
Note2: You should keep observer so after time you can remove it so that can not take extra load on memory
Hope it is helpful
Try calling the view count after player had started playing
func updateView() {
/// Not here Because at this time player current item is not initiated yet
/// if you use Breakpoints in viewCount code you will see it won't enter
/// in if condition created
self.viewcount() /// Comment this line
if let videoUrlString = post?.videoUrl, let videoUrl = URL(string: videoUrlString) {
player = AVPlayer(url: videoUrl)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = postImageView.frame
playerLayer?.frame.size.width = postImageView.frame.size.width
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.contentView.layer.addSublayer(playerLayer!)
/// Player is initiated with a item to play
player?.play()
/// Call current time here
/// Now it will Enter in if Condition
/// Also try using else statement so you know Do control enter in if or in Else
self.viewcount()
}
func viewcount()
{
if let currentitem = player?.currentItem
{
///Yes Player have a item whose time can be Detected
if currentitem.currentTime() == CMTimeMake(3, 1)
{
print ("VIDEO PLAYED FOR THREE SECONDS")
}
}
else
{
/// Check do Control reach here in case 1 When you are calling before player.play()
}
}

Best way to prevent AVPlayer playing more than one track at a time

I'm using AVPlayer to play streaming audio and would like to prevent it from playing more than one audio track at a time. What's the best way to do this? My approach is entirely programmatic -- I don't use the storyboard. When an url is sent to the ViewController (VC, the delegate) it sets up a new instance of the player -- and I think this is the problem, but I'm not sure. Thanks.
//this routine returns the url from control to the VC (delegate)
func selectedAudio(_ audioUrl:String?) {
if let urlString = audioUrl {
setupPlayer(urlString)
}
}
//single button which toggles between play/pause
func playOrPauseTouched() {
if isPlaying {
pausePlayer()
} else {
playPlayer()
}
}
func playPlayer() {
if player == nil || selectedTrackName != currentTrackLabel.text{
if let urlString = selectedAudioUrl {
setupPlayer(urlString)
currentTrackLabel.text = "Playing: \(selectedTrackName ?? "")"
}
}
player?.play()
isPlaying = true
playPauseButton.setImage(UIImage(named: "pause"), for: .normal)
activityIndicatorView.startAnimating()
}
func pausePlayer() {
player?.pause()
isPlaying = false
playPauseButton.setImage(UIImage(named: "play"), for: .normal)
}
func setupPlayer(_ urlString:String) {
if let url = URL(string:urlString) {
player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: player)
player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
let interval = CMTime(value: 1, timescale: 5) //gives smooth thumb movement w/short clips
player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (progressTime) in
let seconds = CMTimeGetSeconds(progressTime)
let secondsString = String(format: "%02d", Int(seconds.truncatingRemainder(dividingBy: 60)))
let minutesString = String(format: "%02d", Int(seconds / 60))
self.currentTimeLabel.text = "\(minutesString):\(secondsString)"
if let duration = self.player?.currentItem?.duration {
let durationSeconds = CMTimeGetSeconds(duration)
self.audioSlider.value = Float(seconds / durationSeconds)
}
})
}
}
My opinion is use KVO that will notify you for current state of AVPlayerItem.
Means you will get state that will help you to identify current item is playing, pushed, buffering or finish to play.
So you should take one flag that will handle your current item state.
Ex.
Set flag true when item is playing
Set flag false when item finish to play.
When you tap on button for play another item then check with flag like if flag == false then start playing another item otherwise not.
Second Way:
Use NotificationCenter like below
NotificationCenter.default.addObserver(self, selector: Selector(("playerDidFinishPlaying:")),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
and below automatically call when item finish playing
func playerDidFinishPlaying(note: NSNotification) {
print("Item finished playing ")
// Set flag here
}
And managed flag as same.

AVPlayer seek right after it starts to play

I need to jump to particular time in audiofile right after it starts to play. So I use seekToTime method with completion handler
avPlayer.play()
...
avPlayer?.seekToTime(jumpTime, completionHandler: { isComplete in
if isComplete {
MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds((self.avPlayer!.currentItem?.currentTime())!)
}
})
The problem is that it needs time to start playing file from the internet. And for some reason the version of seekToTime with completion handler crashes the app, because it's invoked before avPlayer started to play. The version without completion handler works fine.
Error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'AVPlayerItem cannot service a seek request with a completion handler until its status is AVPlayerItemStatusReadyToPlay.'
Is there a way to use a callback with avPlayer.play()?
Yes, there is a way.
You need to observe changes in player:
avPlayer?.addPeriodicTimeObserverForInterval(CMTime(value: 1, timescale: 3), queue: dispatch_get_main_queue()) { [weak self] time in
self?.handleCallback(time)
}
And then you handle the state in the callback:
func handleCallback(time: CMTime) {
if avPlayer?.status == .ReadyToPlay {
// place your seek logic here
}
}
Please look at the Apple's answer for it:
https://developer.apple.com/library/content/qa/qa1820/_index.html
class MyClass {
var isSeekInProgress = false
let player = <#A valid player object #>
var chaseTime = kCMTimeZero
// your player.currentItem.status
var playerCurrentItemStatus:AVPlayerItemStatus = .Unknown
...
func stopPlayingAndSeekSmoothlyToTime(newChaseTime:CMTime)
{
player.pause()
if CMTimeCompare(newChaseTime, chaseTime) != 0
{
chaseTime = newChaseTime;
if !isSeekInProgress
{
trySeekToChaseTime()
}
}
}
func trySeekToChaseTime()
{
if playerCurrentItemStatus == .Unknown
{
// wait until item becomes ready (KVO player.currentItem.status)
}
else if playerCurrentItemStatus == .ReadyToPlay
{
actuallySeekToTime()
}
}
func actuallySeekToTime()
{
isSeekInProgress = true
let seekTimeInProgress = chaseTime
player.seekToTime(seekTimeInProgress, toleranceBefore: kCMTimeZero,
toleranceAfter: kCMTimeZero, completionHandler:
{ (isFinished:Bool) -> Void in
if CMTimeCompare(seekTimeInProgress, chaseTime) == 0
{
isSeekInProgress = false
}
else
{
trySeekToChaseTime()
}
})
}
}

Resources