Swift AVPlayer - The proper way to loop a video with custom rate - ios

So I have an AVPlayer which I would like to continuously loop. This is not an issue as this works fine.
However, the user has the ability to edit the rate of the video. If the rate is changed, it will loop fine for 2-3 times and then the AVPlayer get stuck on the first frame.
The completion block is not called after it gets stuck.
Here is my code for looping the AVPlayer
// Loop video notification
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main) { [weak self] _ in
guard let self = self, let player = self.player else { return }
player.seek(to: .zero)
player.play()
player.rate = self.playerRate
}

Related

Play a video from url and validate if whole video was watched

I have this json and I have to play a video
What is the best way to play video and validate or check if user watched whole video?Video could be (Tiktok - Vimeo - Dayli Motion) video but no Youtube Video
I tried to use AVPlayer but it doesn't work :
let videoURL = NSURL(string: "https://vimeo.com/601097657")
let player = AVPlayer(url: videoURL! as URL)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
playerViewController.player!.play()
}
I think the possible solution it could be a webView but I'm not sure if its possible to validate if user watched whole video
AVPlayer sends notifications on occasions like that.
Simply subscribe to notifications you need. In your case you need
NSNotification.Name.AVPlayerItemDidPlayToEndTime
implementing this would look something like this:
NotificationCenter.default.addObserver(self,
selector: #selector(itemDidPlayToEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: nil)
And implement a selector for handling notification:
#objc private func itemDidPlayToEnd() {
// do smth
}
This is working for me.
class Player: AVPlayerViewController {
init(url: URL) {
super.init(nibName: nil, bundle: nil)
player = AVPlayer(url: url)
player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main, using: { time in
if self.player?.currentItem?.status == .readyToPlay {
let currenTime = CMTimeGetSeconds((self.player?.currentTime())!)
let secs = Int(currenTime)
print(NSString(format: "%02d:%02d", secs/60, secs%60) as String)
}
})
}
This will print out how much of the video they have watched in seconds, which you could then check if this is equal to the total amount of time the video is.
Obviously you would have to implement methods to check if they have skipped part of the video or not, but that's for another question.
Check out more info Time watched. and Total Video Time

How to loop videos with AVQueuePlayer after it completes

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)
}

Play video playlist and individual video clip from playlist in Loop, AVQueuePlayer iOS Swift

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.

One AVPlayer's AVPlayerItemDidPlayToEndTime action executed for all Currently playing videos

Problem : In collectionview cell which has player
if I play two Video simultaneously and seek first Video to end then AVPlayerItemDidPlayToEndTime fired for two times and both videos restarted
In collection view cell I have
override func awakeFromNib() {
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main, using: {[weak self] (notification) in
if self?.player != nil {
self?.player?.seek(to: kCMTimeZero)
self?.player?.play()
}
})
}
and one play button action which play the video.
In cell I have slider to seek.
Any Help would be appreciated
Make sure that player and player?.currentItem are not equal to nil when you're registering for notifications. To me, it seems like one of them was nil and you're basically subscribing to all of the .AVPlayerItemDidPlayToEndTime notifications (since object is nil).
To avoid that, subscribe to the notifications right after assigning AVAsset to the player.
Swift 5.1
Pass the item as your object:
// Stored property
let player = AVPlayer(url: videoUrl)
// When you are adding your video layer
let playerLayer = AVPlayerLayer(player: player)
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
// add layer to view
Then when you get that notification, here's how you can get the currentItem:
// Grab the item from the notification object and ensure its the same item as the current players item
if let item = notification.object as? AVPlayerItem,
let currentItem = player.currentItem,
item == currentItem {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
// remove player from view or do whatever you need to do here
}

Detect if AVPlayerViewController is playing video or buffering and add overlay to the player

I have to detect whether the video is in playing or buffering mode.I am loading the video from a URL. I have tried the below code and I am able to track after once the video has started playing, but not when it is in buffering state.
Also, I want to add an overlay view in my player. I have tried to add the overlay in AVPlayer but when in full screen mode the overlay disappears.
Need some suggestions. Thanks in advance. Below is my code:
let playerAV = AVPlayerViewController()
var player = AVPlayer()
player = AVPlayer(URL: url)
print(url)
playerAV.player = player
playerAV.view.frame = CGRectMake(0, 0, self.videoView.frame.width, self.videoView.frame.height)
self.addChildViewController(playerAV)
self.videoView.addSubview(playerAV.view)
playerAV.didMoveToParentViewController(self)
playerAV.player?.play()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChannelDetailViewController.notificationObserver(_:)), name:AVPlayerItemDidPlayToEndTimeNotification , object: player.currentItem)
_ = UIDevice.beginGeneratingDeviceOrientationNotifications
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChannelDetailViewController.deviceOrientationDidChange(_:)) , name:
UIDeviceOrientationDidChangeNotification, object: nil)
player.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.New, context: nil)
player.addPeriodicTimeObserverForInterval(CMTime(value: 1, timescale: 3), queue: dispatch_get_main_queue()) { [weak self] time in
self?.handlePlayerStatus(time)
}
func handlePlayerStatus(time: CMTime) {
if player.status == .ReadyToPlay {
// buffering is finished, the player is ready to play
print("playing")
}
if player.status == .Unknown{
print("Buffering")
}
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "rate" {
if let rate = change?[NSKeyValueChangeNewKey] as? Float {
if player.currentItem!.status == AVPlayerItemStatus.ReadyToPlay{
if rate != 0 && player.error == nil {
print("normal playback")
}
else{
print("playback stopped")
}
}else if player.currentItem?.status == AVPlayerItemStatus.Unknown{
print("test")
}
}
}
print("you are here")
}
check here for project
Check my answer: https://stackoverflow.com/a/38867386/5109911, this shows you how check if the player is loading the buffer, to check if it is ready to play you have to check both player.currentItem.status and player.status
To add an overlay to AVPlayer I suggest using an UIView above your AVPlayer layer.
I took a slightly different approach to a similar problem - updating the UI when the AVPlayer starts actual playback (in our case, it was audio playback, but the same principle would apply).
We used a boundary time to execute a block at the start of playback – specifically after 1/3 of a second of playback – to update the UI at the same time as audible playback:
let times = [NSValue(time:CMTimeMake(1,3))]
_ = self.player.addBoundaryTimeObserver(forTimes: times, queue: DispatchQueue.main, using: {
[weak self] time in
// Code to update the UI goes here, e.g.
self?.someUIView.isHidden = false
})
I have a little more detail on the approach (and a sample Xcode project) on our company blog: http://www.artermobilize.com/blog/2017/02/09/detect-when-ios-avplayer-finishes-buffering-using-swift/
As to question #2, I concur with Marco's suggestion of using an UIView overtop of the AVPlayer layer.

Resources