Play video from new URL without creating a new AVPlayer object - ios

I'm trying to allow users to be able to cycle through videos, changing the AVPlayer URL on the fly without refreshing the view. However, right now I'm just instantiating AVPlayer objects every time a video is played (resulting in audio to be played over one another), which I feel isn't the best way to do this. Is there a more efficient way similar to changing the image in an imageView?
This is the code where I play the clip:
player = AVPlayer(URL: fileURL)
playerLayer = AVPlayerLayer(player: player)
playerLayer!.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer!)
player!.play()

Do not use an AVPlayer.
Instead use an AVQueuePlayer which allows you to insert and remove items from a queue.
//create local player in setup methods
self.localPlayer = AVQueuePlayer.init()
to add items you can simply use
//optional: clear current queue if playing straight away
self.localPlayer.removeAllItems()
//get url of track
let url : URL? = URL.init(string: "http://urlOfItem")
if url != nil {
let playerItem = AVPlayerItem.init(url: url!)
//you can use the after property to insert
//it at a specific location or leave it nil
self.localPlayer.insert(playerItem, after: nil)
self.localPlayer.play()
}
AVQueuePlayer supports all of the functionality of the AVPlayer but has the added functionality of adding and removing items from a queue.

Use AVPlayerItem to add and remove outputs to an AVPlayer object.
Instead of adding a video to the AVPlayer when you create it, create an empty AVPlayer instance, and then use the addOutput method of the AVPlayerItem class to add the video.
To remove the video and add a new one, use the removeOutput method of the AVPlayerItem class to remove the old video, and then the addOutput method again to insert the new one.
Sample code is available from Apple's developer site at;
https://developer.apple.com/library/prerelease/content/samplecode/AVBasicVideoOutput/Introduction/Intro.html
It provides the same thing I would, were I to post code of my own.

Create AVPlayer Instance globally then override it again when you want to play a new video from new URL.

I am able to accomplish what you are looking for by doing this...
I have a tableView of song names, for which the mp3 files are stored on Parse.com. In didSelectRowAtIndexPath I do...
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
SelectedSongNumber = indexPath.row
grabSong()
}
func grabSong () {
let songQuery = PFQuery(className: "Songs")
songQuery.getObjectInBackgroundWithId(iDArray[SelectedSongNumber], block: {
(object: PFObject?, error : NSError?) -> Void in
if let audioFile = object?["SongFile"] as? PFFile {
let audioFileUrlString: String = audioFile.url!
let audioFileUrl = NSURL(string: audioFileUrlString)!
myAVPlayer = AVPlayer(URL: audioFileUrl)
myAVPlayer.play()
currentUser?.setObject(audioFileUrlString, forKey: "CurrentSongURL")
currentUser?.saveInBackground()
}
})
}
when I run this, i select a row and the song starts playing. If i then wait a few seconds and select a different row, the AVPlayer plays the song from the new cell that i selected and does NOT play one song over the other. My AVPlayer is declared as a public variable for all classes to see.

Related

addPeriodicTimeObserver keeps AVPlayer instances

After being all over Stack Overflow and the deepest corners of the internet, I'm yet to find the solution. I hope someone out there will be able help me out with a problem I've had for days now.
The app in question has a collection view with lots of items. When you click on it you get to a preview collection view. And finally (where I need help) when you click "GO" you come to a collection view with cells that fills out the entire screen. It consists of an AVPlayerLayer and AVPlayer. Each time you scroll to right or left you see another video. The following code works:
UICollectionViewCell
class PageCell: UICollectionViewCell {
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
var playerItem: AVPlayerItem?
var videoAsset: AVAsset?
var indexPath: IndexPath?
var page: Page? {
didSet {
guard let unwrappedPage = page, let unwrappedIndexPath = indexPath else { return }
addingVideo(videoID: unwrappedPage.steps[unwrappedIndexPath.item].video)
}
}
func setupPlayerView() {
player = AVPlayer()
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = .resizeAspectFill
videoView.layer.addSublayer(playerLayer!)
layoutIfNeeded()
playerLayer?.frame = videoView.bounds
}
func addingVideo(videoID: String) {
setupPlayerView()
guard let url = URL(string: videoID) else { return }
videoAsset = AVAsset(url: url)
activityIndicatorView.startAnimating()
videoAsset?.loadValuesAsynchronously(forKeys: ["duration"]) {
guard self.videoAsset?.statusOfValue(forKey: "duration", error: nil) == .loaded else { return }
self.player?.play()
self.playerItem = AVPlayerItem(asset: self.videoAsset!)
self.player?.replaceCurrentItem(with: self.playerItem)
}
}
In the UICollectionViewController I'm reusing cells. Since there is no way to know the number of AVPlayer instances, I simply made a helper variable inside the PageCell and it seems like three cells are getting reused (the normal amount when dequeuing and reusing cells). Now when I close this UICollectionViewController the AVPlayer instances seems to disappear/close.
Now, the problem arises when I want to loop the videos. Using AVPlayerLooper is not an option because it simply is too laggy (I've implemented it in a dusin different ways without luck). So my solution was to use a period time observer inside the videoAsset?.loadValuesAsynchronously block:
self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
queue: DispatchQueue.global(), using: { (progressTime) in
if let totalDuration = self.player?.currentItem?.duration{
if progressTime == totalDuration {
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
}
})
Problem
Video showcasing problem: Problem video
The problem arises when having +17 AVPlayer instances running simultaneously then suddenly the videos won't load anymore. iOS devices have a hardware limitation of everything from 4 to 18 AVPlayer instances running at the same time (most likely depending on RAM), see the following Stack Overflow posts just to mention a few:
AVPlayerItemStatusFailed error when too many AVPlayer instances created
How many AVPlayers are allowed to be created at the same time?
AVPlayer not able to play after playing some items
Loading issues with AVPlayerItem from local URL
See also these articles for more insight:
Building native video Pins
Too Many AVPlayers?
Because the problem only occurs when adding the time observer I suspect that it keeps the AVPlayer instances "alive" even though I've closed the collection view.
Notes to Problem video: Every time I press "GO" one AVPlayer instance is created. When I swipe to the right two more cells are created hence two more AVPlayer instances. All in all 3 AVPlayer instances are created each time accumulating in the end to about 17 instances and the videos will not load anymore. I've tried scrolling through all the videos each time I've pressed "GO" but this does not change the outcome with a maximum of three cells being reused all the time.
What I've tried to solve the problem
Try 1:
I made playerLayer a global variable and in viewDidDisappear inside the UICollectionViewController I added:
playerLayer?.player = nil
This resulted in one AVPlayer instance to disappear/close when I closed the cells onto the "GO" page (i.e. the view disappeared). This meant that I hit 17 instances a bit later than when I didn't add this code. Together with the code above I also tried adding playerLayer = nil, playerLayer?.player?.replaceCurrentItem(with: nil) and playerLayer?.removeFromSuperlayer() but the only thing that changed anything was playerLayer?.player = nil. It should be noted that if I do not scroll and simply open the first video and close, open the first video and close and so on, I could do it "forever" without any problems. So, in this case one instance was created and then closed/disappeared afterwards.
Video showcasing try 1: Try 1 video
Try 2:
I changed the addPeriodicTimeObserver block to:
self.timeObserver = playerLayer?.player?.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale:
2), queue: DispatchQueue.global(), using: { (progressTime) in
if let totalDuration = playerLayer?.player?.currentItem?.duration{
if progressTime == totalDuration {
playerLayer?.player?.seek(to: kCMTimeZero)
playerLayer?.player?.play()
}
}
})
In this block I essentially changed all self.player? to playerLayer?.player?.
This made me able to open and close the view with the videos as much as I wanted - so the AVPlayer instances were somehow closing/disappearing. Though now the looping didn't work. The first video would loop initially. But then I swiped to the second cell and this video would not loop. Then I swiped back to the first cell and now this wouldn't loop either. If I added playerLayer?.player = nil like in "Try 1" no effect occurred with neither the open and close nor the loops.
Video showcasing try 2: Try 2 video
Try 3:
I made the timeObserver variable global and tried many many things. But when I tried to remove the observer(s) it always resulted in the following error:
'NSInvalidArgumentException', reason: 'An instance of AVPlayer cannot remove a time observer that was added by a different instance of AVPlayer.'
The error indicates that the time observers are clearly all over the place. This was clear when I added helper variables (counters) inside the addPeriodicTimeObserver block and found out that the time observers were quickly adding up. At one point with all the different combinations of the code presented in this post, I was able to remove the time observer at any time during the scrolling. But this resulted in only removing a single time observer out of many - resulting in the same situation as in "Try 1" where I was able to remove a single AVPlayer instance each time I closed the view.
General note:
To test whether or not it really was only three AVPlayer instances that were created each time I press "GO" and swiped I made a test where I scrolled through over 20 videos, but there was no problem loading them. Therefore I'm quite sure, as mentioned earlier, that a maximum of three AVPlayer instances are created in the view each time.
Conclusion
The problem is, as I see it, that when I add the time observers in order to loop the videos they accumulate and keep the AVPlayer instances "alive". Remember that I had no problem at all without the time observers. When applying playerLayer?.player = nil it seems as though, I was able to "save" an instance but then again it's so hard to tell when I don't know the amount of AVPlayer instances currently "active". Simply put, all I wanna do is to delete all AVPlayer instances the moment the view disappears and it will be happy days. If anyone got this far, I will be overly grateful if you are able to help me out.
There is a retain cycle. Use weak self in escaping closures. The retain cycle you created was:
cell -> Player -> observer closure -> cell
Try this:
self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
queue: DispatchQueue.global(), using: { [weak self] (progressTime) in
if let totalDuration = self?.player?.currentItem?.duration{
if progressTime == totalDuration {
self?.player?.seek(to: kCMTimeZero)
self?.player?.play()
}
}
})

AVPlayer with Visualizer

I need to create a Audio Player for streamed URL (m3u8 format). I have created music player using AVPlayer. But I need to show visualizer for streamed song. I have tried different solution but not found any working example of it.
I have created visualizer using AVAudioPlayer(averagePower) but it won't support streamed URL.
Any help to show visualizer for AVPlayer? Thanks in advance.
I have also tried using MYAudioTapProcessor which most of the people suggested, but for streamed URL, tracks always returns null.
Added the MYAudioTapProcessor.h and MYAudioTapProcessor.m in project
//Initialization of player
let playerItem = AVPlayerItem( url:NSURL( string:"https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8" ) as! URL )
let audioPlayer: AVPlayer = AVPlayer(playerItem:playerItem)
//Added periodic time observer
audioPlayer!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if audioPlayer!.currentItem?.status == .readyToPlay
{
if let playerItem: AVPlayerItem = audioPlayer!.currentItem {
print(playerItem.asset.tracks.count)
if (playerItem.asset.tracks) != nil {
self.tapProcessor = MYAudioTapProcessor(avPlayerItem: playerItem)
playerItem.audioMix = self.tapProcessor.audioMix
self.tapProcessor.delegate = self
}
}
}
}
//Delegate callback method for MYAudioTapProcessor
func audioTabProcessor(_ audioTabProcessor: MYAudioTapProcessor!, hasNewLeftChannelValue leftChannelValue: Float, rightChannelValue: Float) {
print("volume: \(leftChannelValue) : \(rightChannelValue)")
volumeSlider.value = leftChannelValue
}
Also tried by adding the "Track" observer.
playerItem.addObserver(self, forKeyPath: "tracks", options: NSKeyValueObservingOptions.new, context: nil);
Now if play mp3 file, the callback method calls but for m3u8 callback method doesn't call. The main reason for failing m3u8 URL is it always show tracks array count zero whereas for mp3 files tracks array has one item.
You cannot get tracks for HLS via AVPLayer. You should use progressive download or local file for getting audio tracks while playing media.

Using NotificationCenter to alternate videos files in AVPlayer

I'm working on implementing a video player in Swift that will detect if a video has stopped playing, and then play the second one. When the second one has stopped playing, the first video should play again.
Here's where I set up the player, assets, and player items:
//Create URLs
let movieOneURL: URL = URL(fileURLWithPath: movieOnePath)
let movieTwoURL: URL = URL(fileURLWithPath: movieTwoPath)
//Create Assets
let assetOne = AVAsset(url: movieOneURL)
let assetTwo = AVAsset(url: movieTwoURL)
//Create Player Items
avPlayerItemOne = AVPlayerItem(asset: assetOne)
avPlayerItemTwo = AVPlayerItem(asset: assetTwo)
avplayer = AVPlayer(playerItem: avPlayerItemOne)
let avPlayerLayer = AVPlayerLayer(player: avplayer)
avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
avPlayerLayer.frame = UIScreen.main.bounds
movieView.layer.addSublayer(avPlayerLayer)
//Config player
avplayer .seek(to: kCMTimeZero)
avplayer.volume = 0.0
And here's where I set up a notification to detect if the player reached the end of the video file:
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: avplayer.currentItem)
...which calls this selector:
func playerItemDidReachEnd(_ notification: Notification) {
// avplayer.seek(to: kCMTimeZero)
changePlayerAsset()
// avplayer.play()
}
...which will then switch out the asset:
func changePlayerAsset(){
if avplayer.currentItem == avPlayerItemOne {
avplayer.replaceCurrentItem(with: avPlayerItemTwo)
avplayer.play()
} else if avplayer.currentItem == avPlayerItemTwo {
avplayer.replaceCurrentItem(with: avPlayerItemOne)
avplayer.play()
}
}
This works perfectly the first time through - when the first movie has finished playing, the next one will then start playing.
The problem I'm having is that my notification observer only seems to register once; at the end of the first video...the notification isn't fired when the second video stops playing at all.
Anyone have an idea why that would be the case
The reason your notification handler isn’t getting called for the second item is this bit, at the end of where you register the notification handler: object: avplayer.currentItem. The handler gets called once, when that item finishes playing, but then when the next item finishes, the notification gets posted with a different object—the other item—which doesn’t match what you registered for, and so your handler doesn’t get called. If you change object to nil when you register the handler, it’ll get called when any item finishes, which is closer to what you’re after.
That said, this isn’t a great way to do what you want—manually swapping out items is likely to incur the cost and delay of loading each item each time it’s about to play. You’d be much better off using the built-in functionality for playing videos in sequence and looping them, namely AVQueuePlayer and AVPlayerLooper. There’s an example of how to use both in the answer to this question.

How to close previous AVPlayer and AVPlayerItem

I'm making an iOS app in Swift that plays a video in a loop in a small layer in the top right corner of the screen which shows a video of specific coloured item. the user then taps the corresponding coloured item on the screen. when they do, the videoName variable is randomly changed to the next colour and the corresponding video is triggered. I have no problem raising, playing and looping the video with AVPlayer, AVPlayerItem as seen in the attached code.
Where I'm stuck is that whenever the next video is shown, previous ones stay open behind it. Also, After 16 videos have played, the player disappears altogether on my iPad. I've tried many suggestions presented in this and other sites but either swift finds a problem with them or it just doesn't work.
So question: within my code here, how do I tell it "hey the next video has started to play, remove the previous video and it's layer and free up the memory so i can play as many videos as needed"?
//set variables for video play
var playerItem:AVPlayerItem?
var player:AVPlayer?
//variables that contain video file path, name and extension
var videoPath = NSBundle.mainBundle().resourcePath!
var videoName = "blue"
let videoExtension = ".mp4"
//DISPLAY VIDEO
func showVideo(){
//Assign url path
let url = NSURL(fileURLWithPath: videoPath+"/Base.lproj/"+videoName+videoExtension)
playerItem = AVPlayerItem(URL: url)
player=AVPlayer(playerItem: playerItem!)
let playerLayer=AVPlayerLayer(player: player!)
//setplayser location in uiview and show video
playerLayer.frame=CGRectMake(700, 5, 350, 350)
self.view.layer.addSublayer(playerLayer)
player!.play()
// Add notification to know when the video ends, then replay it again. THIS IS A CONTINUAL LOOP
NSNotificationCenter.defaultCenter().addObserverForName(AVPlayerItemDidPlayToEndTimeNotification, object: player!.currentItem, queue: nil)
{ notification in
let t1 = CMTimeMake(5, 100);
self.player!.seekToTime(t1)
self.player!.play()
}
}
`
I adapted #Anupam Mishra's Swift code suggestion. It wasn't working at first but finally figured I had to take the playerLayer outside the function and close the playerLayer after I set the player to nil. Also. instead of using 'if(player!.rate>0 ...)' which would no doubt still work, I created a variable switch to indicate when to say "kill the player AND the layer" as seen below. It may not be pretty but it works! The following is for absolute newbies like myself - WHAT I LEARNED FROM THIS EXPERIENCE: it seems to me that an ios device only allows 16 layers to be added to a viewController (or superLayer). so each layer needs to be deleted before opening the next layer with its player unless you really want 16 layers running all at once. WHAT THIS CODE BELOW ACTUALLY DOES FOR YOU: this code creates a re-sizable layer over an existing viewController and plays a video from your bundle in an endless loop. When the next video is about to be called, the current video and the layer are totally removed, freeing up the memory, and then a new layer with the new video is played. The video layer size and location is totally customizable using the playerLayer.frame=CGRectMake(left, top, width, height) parameters. HOW TO MAKE IT ALL WORK: Assuming you've already added your videos to you bundle, create another function for your button tap. in that function, first call the 'stopPlayer()' function, change the 'videoName' variable to the new video name you desire, then call the 'showVideo()' function. (if you need to change the video extension, change 'videoExtension' from a let to a var.
`
//set variables for video play
var playerItem:AVPlayerItem?
var player:AVPlayer?
var playerLayer = AVPlayerLayer() //NEW playerLayer var location
//variables that contain video file path, name and extension
var videoPath = NSBundle.mainBundle().resourcePath!
var videoName = "blue"
let videoExtension = ".mp4"
var createLayerSwitch = true /*NEW switch to say whether on not to create the layer when referenced by the closePlayer and showVideo functions*/
//DISPLAY VIDEO
func showVideo(){
//Assign url path
let url = NSURL(fileURLWithPath: videoPath+"/Base.lproj/"+videoName+videoExtension)
playerItem = AVPlayerItem(URL: url)
player=AVPlayer(playerItem: playerItem!)
playerLayer=AVPlayerLayer(player: player!) //NEW: remove 'let' from playeLayer here.
//setplayser location in uiview and show video
playerLayer.frame=CGRectMake(700, 5, 350, 350)
self.view.layer.addSublayer(playerLayer)
player!.play()
createLayerSwitch = false //NEW switch to tell if a layer is already created or not. I set the switch to false so that when the next tapped item/button references the closePlayer() function, the condition is triggered to close the player AND the layer
// Add notification to know when the video ends, then replay it again without a pause between replays. THIS IS A CONTINUAL LOOP
NSNotificationCenter.defaultCenter().addObserverForName(AVPlayerItemDidPlayToEndTimeNotification, object: player!.currentItem, queue: nil)
{ notification in
let t1 = CMTimeMake(5, 100);
self.player!.seekToTime(t1)
self.player!.play()
}
}
//NEW function to kill the current player and layer before playing the next video
func closePlayer(){
if (createLayerSwitch == false) {
player!.pause()
player = nil
playerLayer.removefromsuperlayer()
}
}
`
Why don't just use replaceCurrentItemWithPlayerItem on your player ? You will keep the same player for all your videos. I think it's a better way to do.
Edit : replaceCurrentItemWithPlayerItem has to be call on the main thread
Before moving on next track first check, is player having any videos or music, to stop it do the following checks:
Swift Code-
if player!.rate > 0 && player!.error == nil
{
player!.pause()
player = nil
}
Objective-C Code-
if (player.rate > 0 && !player.error)
{
[player setRate:0.0];
}

AVPlayer "freezes" the app at the start of buffering an audio stream

I am using a subclass of AVQueuePlayer and when I add new AVPlayerItem with a streaming URL the app freezes for about a second or two. By freezing I mean that it doesn't respond to touches on the UI. Also, if I have a song playing already and then add another one to the queue, AVQueuePlayer automatically starts preloading the song while it is still streaming the first one. This makes the app not respond to touches on the UI for two seconds just like when adding the first song but the song is still playing. So that means AVQueuePlayer is doing something in main thread that is causing the apparent "freeze".
I am using insertItem:afterItem: to add my AVPlayerItem. I tested and made sure that this was the method that was causing the delay. Maybe it could be something that AVPlayerItem does when it gets activated by AVQueuePlayer at the moment of adding it to the queue.
Must point out that I am using the Dropbox API v1 beta to get the streaming URL by using this method call:
[[self restClient] loadStreamableURLForFile:metadata.path];
Then when I receive the stream URL I send it to AVQueuePlayer as follows:
[self.player insertItem:[AVPlayerItem playerItemWithURL:url] afterItem:nil];
So my question is: How do I avoid this?
Should I do the preloading of an audio stream on my own without the help of AVPlayer? If so, how do I do this?
Thanks.
Don't use playerItemWithURL it's sync.
When you receive the response with the url try this:
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
NSArray *keys = #[#"playable"];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() {
[self.player insertItem:[AVPlayerItem playerItemWithAsset:asset] afterItem:nil];
}];
Bump, since this is a highly rated question and similar questions online either has outdated answers or aren't great. The whole idea is pretty straight forward with AVKit and AVFoundation, which means no more depending on third party libraries. The only issue is that it took some tinkering around and put the pieces together.
AVFoundation's Player() initialization with url is apparently not thread safe, or rather it's not meant to be. Which means, no matter how you initialize it in a background thread the player attributes are going to be loaded in the main queue causing freezes in the UI especially in UITableViews and UICollectionViews. To solve this issue Apple provided AVAsset which takes a URL and assists in loading the media attributes like track, playback, duration etc. and can do so asynchronously, with a best part being that this loading process is cancellable (unlike other Dispatch queue background threads where ending a task may not be that straight forward). This means, there is no need to worry about lingering zombie threads in the background as you scroll fast on a table view or collection view, ultimately piling up on the memory with a whole bunch of unused objects. This cancellable feature is great, and allows us to cancel any lingering AVAsset async load if it is in progress but only during cell dequeue. The async loading process can be invoked by the loadValuesAsynchronously method, and can be cancelled (at will) at any later time (if still in progress).
Don't forget to exception handle properly using the results of loadValuesAsynchronously. In Swift (3/4), here's how you would would load a video asynchronously and handle situations if the async process fails (due to slow networks, etc.)-
TL;DR
TO PLAY A VIDEO
let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
let keys: [String] = ["playable"]
var player: AVPlayer!
asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: "playable", error: &error)
switch status {
case .loaded:
DispatchQueue.main.async {
let item = AVPlayerItem(asset: asset)
self.player = AVPlayer(playerItem: item)
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
self.player.isMuted = true
self.player.play()
}
break
case .failed:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
case .cancelled:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
default:
break
}
})
NOTE:
Based on what your app wants to achieve you may still have to do some amount of tinkering to tune it to get smoother scroll in a UITableView or UICollectionView. You may also need to implement some amount of KVO on the AVPlayerItem properties for it to work and there's plenty of posts here in SO that discuss AVPlayerItem KVOs in detail.
TO LOOP THROUGH ASSETS (video loops/GIFs)
To loop a video, you can use the same method above and introducing AVPlayerLooper. Here's a sample code to loop a video (or perhaps a short video in GIF style). Note the use of duration key which is required for our video loop.
let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
let keys: [String] = ["playable","duration"]
var player: AVPlayer!
var playerLooper: AVPlayerLooper!
asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: "duration", error: &error)
switch status {
case .loaded:
DispatchQueue.main.async {
let playerItem = AVPlayerItem(asset: asset)
self.player = AVQueuePlayer()
let playerLayer = AVPlayerLayer(player: self.player)
//define Timerange for the loop using asset.duration
let duration = playerItem.asset.duration
let start = CMTime(seconds: duration.seconds * 0, preferredTimescale: duration.timescale)
let end = CMTime(seconds: duration.seconds * 1, preferredTimescale: duration.timescale)
let timeRange = CMTimeRange(start: start, end: end)
self.playerLooper = AVPlayerLooper(player: self.player as! AVQueuePlayer, templateItem: playerItem, timeRange: timeRange)
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
self.player.isMuted = true
self.player.play()
}
break
case .failed:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
case .cancelled:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
default:
break
}
})
EDIT : As per the documentation, AVPlayerLooper requires the duration property of the asset to be fully loaded in order to be able to loop through videos. Also, the timeRange: timeRange with the start and end timerange in the AVPlayerLooper initialization is really optional if you want an infinite loop. I have also realized since I posted this answer that AVPlayerLooper is only about 70-80% accurate in looping videos, especially if your AVAsset needs to stream the video from a URL. In order to solve this issue there is a totally different (yet simple) approach to loop a video-
//this will loop the video since this is a Gif
let interval = CMTime(value: 1, timescale: 2)
self.timeObserverToken = self.player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (progressTime) in
if let totalDuration = self.player?.currentItem?.duration{
if progressTime == totalDuration{
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
}
})
Gigisommo's answer for Swift 3 including the feedback from the comments:
let asset = AVAsset(url: url)
let keys: [String] = ["playable"]
asset.loadValuesAsynchronously(forKeys: keys) {
DispatchQueue.main.async {
let item = AVPlayerItem(asset: asset)
self.playerCtrl.player = AVPlayer(playerItem: item)
}
}

Resources