I am building off of the AVPlayerLooper example code Apple provides, specifically utilizing the example AVPlayerLooper setup they've given you in PlayerLooper.swift, LooperViewController.swift, and the Looper.swift protocol.
What I would like to do is be able to update the timeRange property on the AVPlayerLooper that is instantiated inside of the PlayerLooper.swift file.
To this end, I have slightly modified the following function which instantiates and starts the player looper:
func start(in parentLayer: CALayer, loopTimeRange: CMTimeRange) {
player = AVQueuePlayer()
playerLayer = AVPlayerLayer(player: player)
guard let playerLayer = playerLayer else { fatalError("Error creating player layer") }
playerLayer.frame = parentLayer.bounds
parentLayer.addSublayer(playerLayer)
let playerItem = AVPlayerItem(url: videoURL)
playerItem.asset.loadValuesAsynchronously(forKeys: [ObserverContexts.playerItemDurationKey], completionHandler: {()->Void in
/*
The asset invokes its completion handler on an arbitrary queue when
loading is complete. Because we want to access our AVPlayerLooper
in our ensuing set-up, we must dispatch our handler to the main queue.
*/
DispatchQueue.main.async(execute: {
guard let player = self.player else { return }
var durationError: NSError? = nil
let durationStatus = playerItem.asset.statusOfValue(forKey: ObserverContexts.playerItemDurationKey, error: &durationError)
guard durationStatus == .loaded else { fatalError("Failed to load duration property with error: \(String(describing: durationError))") }
//self.playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
self.playerLooper = AVPlayerLooper(player: player, templateItem: playerItem, timeRange: loopTimeRange)
self.startObserving()
player.play()
})
})
}
For demonstration purposes, in LooperViewController, I created a simple button that triggers looper?.start() with a new CMTimeRange like so:
looper?.start(in: view.layer, loopTimeRange: CMTimeRange(start: CMTime(value: 0, timescale: 600), duration: CMTime(value: 3000, timescale: 600)))
Before that function is called, however, I call looper?.stop(), which does the following:
func stop() {
player?.pause()
stopObserving()
playerLooper?.disableLooping()
playerLooper = nil
playerLayer?.removeFromSuperlayer()
playerLayer = nil
player = nil
}
I'm basically completely re-instantiating the AVPlayerLooper in order to set the new timeRange property because I don't see any way to actually access, and reset, that property once it's been setup the first time.
The problem is that, while this seems to initially work and the looper player will adjust and start looping the new timerange, it will eventually just stop playing after a few loops. No errors are thrown anywhere, and none of the observers that are already setup in the code are reporting that the loop is stopping or that there was some error with the loop.
Is my approach here entirely wrong? Is AVPlayerLooper meant to be adjusted in this way or should I look for another approach to having an adjustable looping player?
You can actually update the AVPlayerLooper without tearing down the whole thing. What you need to do is to remove all items from the AVQueuePlayer first and then reinstantiate the looper with the new time range. Something like this:
if self.avQueuePlayer.rate == 0 {
self.avQueuePlayer.removeAllItems()
let range = CMTimeRange(start: self.startTime, end: self.endTime)
self.avPlayerLooper = AVPlayerLooper(player: self.avQueuePlayer, templateItem: self.avPlayerItem, timeRange: range)
self.avQueuePlayer.play()
}
You must be sure to removeAllItems() or it will crash. Otherwise this will change the time range while allowing you to use the current layer, etc., setup to view the player.
Related
I run the code
DispatchQueue.main.async { [weak self] in
self?.sliderView.elapsedLabel.text = self?.player.currentTime.secondsToString()
}
Which every second of an observer firing that watches the audio file progression, will update a label of remaning seconds. It forces main thread because its UI related...
player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
Meanwhile I have an animation loop playing in the background using AVKit
I am finding that, when the label updates, it stops the animation, as both are real time main thread updates?
How is this handled to allow 2 processes to update the UI at the same time without clashing with each other?
Updated code for the animating video:
let playerItem = AVPlayerItem(url: file)
videoPlayer = AVQueuePlayer(items: [playerItem])
playerLooper = AVPlayerLooper(player: videoPlayer!, templateItem: playerItem)
videoPlayerLayer = AVPlayerLayer(player: videoPlayer)
videoPlayerLayer!.frame = inView.bounds
videoPlayerLayer!.videoGravity = AVLayerVideoGravity.resizeAspectFill
then I call the standard
videoPlayer?.play()
I have an array of URLs that I then turn into an array of AVPlayerItems and use AVQueuePlayer to loop through the videos- usually 1-7 videos at a time. However when it stops I am not sure how to start it again to play the same array of videos until the user switches to a different view controller.
in viewDidLoad this creates the array of playerItems
//creates playerItems to play videos in a queue
postURLs?.forEach{(url) in
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
playerItems.append(playerItem)
}
public func playVideo() {
player = AVQueuePlayer(items: playerItems)
player.seek(to: CMTime.init(value: 0, timescale: 1))
playerLayer = AVPlayerLayer(player:player)
playerLayer.frame = self.lifieView.frame
lifieView.layer.addSublayer(playerLayer)
player.play()
//restart video maybe? Tested but did not work - hits function
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: nil,
queue: nil) { [weak self] _ in self?.restart2() }
}
//this is test function to restart (works with AVPlayer with single video)
private func restart2(){
player.seek(to: CMTime.zero)
player.play()
}
I got it working after much research and testing.
What I did was change the restart function to first remove all items from the player, then go through the array of playerItems and add them back into the queue- then have the player start back at the beginning.
func restartPlayer(){
player.removeAllItems()
playerItems.forEach{
player.insert($0, after:nil)
}
player.seek(to: .zero)
}
I'm using an AVPlayer to play a remote progressive download (i.e. non-HLS) video. But, I can't figure out how to control its buffering behavior.
I would like to pre-fetch 2 seconds of the video before it's ready to play, and also to stop buffering when the video is paused.
Here's my setup:
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer()
I tried the following, without success:
// doesn't start buffering
playerItem.preferredForwardBufferDuration = 2.0
// doesn't stop buffering
playerItem.preferredForwardBufferDuration = 2.0
player.replaceCurrentItem(with: playerItem)
I tried player.automaticallyWaitsToMinimizeStalling = true in both cases, and in combination with various player.pause() or player.rate = 0 - doesn't work.
A potential approach that comes to mind is to observe for loadedTimeRanges until the first 2 seconds loaded and set current item of the player to nil.
let c = playerItem.publisher(for: \.loadedTimeRanges, options: .new)
.compactMap { $0.first as? CMTimeRange }
.sink {
if $0.duration.seconds - $0.start.seconds > 2 {
player.replaceCurrentItem(with: nil)
}
}
This would work for pre-buffer, but it doesn't work for pausing, because it makes the video blank instead of paused. (And at this point, I feel I'm attempting to reimplement/interfere with some core buffering functionality)
I am trying to play video playlist in loop and also play individual video clip from playlist in loop in AVQueuePlayer using AVPlayerItem, but i am unable to find the solution for same below is the code that i have tried so far
General
var player : AVQueuePlayer?
var playerLayer: AVPlayerLayer?
var playerItem: [AVPlayerItem] = []
func playAtIndex(index:Int){
for i in index ..< playerItem.count {
let obj = playerItem[i]
if (self.player?.canInsert(obj, after: nil))! {
obj.seek(to: .zero, completionHandler: nil)
self.player?.insert(obj, after: nil)
}
}
}
Initialise video player
self.player = AVQueuePlayer.init(items: self.playerItem)
self.playerLayer = AVPlayerLayer(player: self.player)
self.playerLayer?.frame = self.view!.bounds
self.playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
self.view!.layer.addSublayer(self.playerLayer!)
self.player?.play()
code done so far for looping playlist, this works but some of the video from the loop does not play sometimes.
self.playAtIndex(index: 0)
code done for looping individual video clip in playlist, but does not work
let playerItem: AVPlayerItem = note.object as! AVPlayerItem // here we get current item
playerItem.seek(to: CMTime.zero, completionHandler: nil)
self.player?.play()
Any help will be great.!!
To loop the playerItems in AVQueuePlayer, you need to add a an observer to the notification - AVPlayerItemDidPlayToEndTimeNotification
A notification that's posted when the item has played to its end time.
NotificationCenter.default.addObserver(self, selector: #selector(playerEndedPlaying), name: Notification.Name("AVPlayerItemDidPlayToEndTimeNotification"), object: nil)
The method playerEndedPlaying(_:) will be called whenever the notification is fired.
#objc func playerEndedPlaying(_ notification: Notification) {
DispatchQueue.main.async {[weak self] in
if let playerItem = notification.object as? AVPlayerItem {
self?.player?.remove(playerItem)
playerItem.seek(to: .zero, completionHandler: nil)
self?.player?.insert(playerItem, after: nil)
if playerItem == self?.playerItems?.last {
self?.pauseVideo()
}
}
}
}
The above method will be called every time a playerItem in the AVQueuePlayer ends playing.
AVQueuePlayer's insert(_:after:) is called where each playerItem is appended to the queue.
afterItem
The player item that the newly inserted player item should
follow in the queue. Pass nil to append the item to the queue.
Loop is identified using playerItem == self?.playerItems?.last. You can add your custom handling here. I've paused the player once all the videos end playing.
I am trying to add reverse playback to video which I am playing with AVPlayer :
let videoURL = Bundle.main.url(forResource: "video", withExtension: "mov")
videoPlayer = AVPlayer(url:videoURL!)
let playerLayer = AVPlayerLayer(player: videoPlayer)
playerLayer.frame = self.view.frame
videoView.layer.addSublayer(playerLayer)
videoPlayer.play()
I searched and found if I change AVPlayer's rate to -1 the movie plays in reverse mode :
func reverseVideo() {
videoPlayer.play()
videoPlayer.rate = -1
}
This does work fine but reverse playback contains lots of lag and it doesn't play smoothly, is there any possible way to fix this issue. I have read other topic in here but did't help.
this code is worked for me to play video in avplayer in reverse from local filesystem.
let duration = (self.playeritem?.asset.duration)!
let durationSec = CMTimeGetSeconds((self.playeritem?.asset.duration)!)
self.avPlayer?.seek(to: duration, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
self.avPlayer?.rate = -1
Try Replacing
videoPlayer.play()
videoPlayer.rate = -1 with
videoPlayer.playImmediately(atRate: -1)
I am not sure if following would work correctly since I have not tried playing an asset in reverse before. I think following is a good idea to try out to seek to end before starting to play.
player.seek(to: player.currentItem!.duration,
toleranceBefore: kCMTimeZero,
toleranceAfter: kCMTimeZero,
completionHandler: { (finished) in
player.play()
player.rate = -1
})
var durTime: CMTime = queuePlayer.currentItem.asset.duration
durationTime = CMTimeGetSeconds(durTime)
if CMTIME_IS_VALID(durTime) {
queuePlayer.seek(toTime: durTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
}
else {
print("In valid time")
}
Now set Rate of AVPlayer to negative value
queuePlayer.rate = -1