I'm working on an application where there is a collection view, and cells of the collection view can contain video. Right now I'm displaying the video using AVPlayer
and AVPlayerLayer
. Unfortunately, the scrolling performance is terrible. It seems like AVPlayer
, AVPlayerItem
, and AVPlayerLayer
do a lot of their work on the main thread. They are constantly taking out locks, waiting on semaphores, etc. which is blocking the main thread and causing severe frame drops.
Is there any way to tell AVPlayer
to stop doing so many things on the main thread? So far nothing I've tried has solved the problem.
I also tried building a simple video player using AVSampleBufferDisplayLayer
. Using that I can make sure that everything happens off the main thread, and I can achieve ~60fps while scrolling and playing video. Unfortunately that method is much lower level, and it doesn't provide things like audio playback and time scrubbing out of the box. Is there any way to get similar performance with AVPlayer
? I'd much rather use that.
Edit:
After looking into this more, it doesn't look like it's possible to achieve good scrolling performance when using AVPlayer
. Creating an AVPlayer
and associating in with an AVPlayerItem
instance kicks off a bunch of work which trampolines onto the main thread where it then waits on semaphores and tries to acquire a bunch of locks. The amount of time this stalls the main thread increases quite dramatically as the number of videos in the scrollview increases.
AVPlayer
dealloc also seems to be a huge problem. Dealloc'ing an AVPlayer
also tries to synchronize a bunch of stuff. Again, this gets extremely bad as you create more players.
This is pretty depressing, and it makes AVPlayer
almost unusable for what I'm trying to do. Blocking the main thread like this is such an amateur thing to do so it's hard to believe Apple engineers would've made this kind of mistake. Anyways, hopefully they can fix this soon.
If you look into Facebook's AsyncDisplayKit (the engine behind Facebook and Instagram feeds) you can render video for the most part on background threads using their AVideoNode. If you subnode that into a ASDisplayNode and add the displayNode.view to whatever view you are scrolling (table/collection/scroll) you can achieve perfectly smooth scrolling (just make sure the create the node and assets and all that on a background thread). The only issue is when having the change the video item, as this forces itself onto the main thread. If you only have a few videos on that particular view you are fine to use this method!
Here's a working solution for displaying a "video wall" in a UICollectionView:
1) Store all of your cells in an NSMapTable (from henceforth, you will only access a cell object from the NSMapTable):
2) Add this method to your UICollectionViewCell subclass:
3) Call the method above from your UICollectionView delegate this way:
By the way, here's how you would populate a PHFetchResult collection with all videos in the Video folder of the Photos app:
If you want to filter videos that are local (and not in iCloud), which is what I'd assume, seeing as you're looking for smooth-scrolling:
Don't know if this will help – but here's some code I'm using to load videos on background queue that definitely helps with main thread blocking (Apologies if it doesn't compile 1:1, I abstracted from a larger code base I'm working on):
I manage to create a horizontal feed like view with
avplayer
in each cell did it like so:Buffering - create a manager so you can preload (buffer) the videos. The amount of
AVPlayers
you want to buffer depends on the experience you are looking for. In my app i manage only 3AVPlayers
, so one player is being played now and the previous & next players are being buffered. All the buffering manager is doing is managing that the correct video is being buffered at any given pointReused cells - Let the
TableView
/CollectionView
reuse the cells incellForRowAtIndexPath:
all you have to do is after you dequqe the cell pass him it's correct player (i just give the buffering an indexPath on the cell and he returns the correct one)AVPlayer
KVO's - Every time the buffering manager gets a call to load a new video to buffer the AVPlayer create all of his assets and notifications, just call them like so:// player
where: videoContainer - is the view you want to add the player to
Let me know if you need any help or more explanations
Good luck :)
Build your
AVPlayerItem
in a background queue as much as possible (some operations you have to do on the main thread, but you can do setup operations and waiting for video properties to load on background queues - read the docs very carefully). This involves voodoo dances with KVO and is really not fun.The hiccups happen while the
AVPlayer
is waiting for theAVPlayerItem
s status to becomeAVPlayerItemStatusReadyToPlay
. To reduce the length of the hiccups you want to do as much as you can to bring theAVPlayerItem
closer toAVPlayerItemStatusReadyToPlay
on a background thread before assigning it to theAVPlayer
.It's been a while since I actually implemented this, but IIRC the main thread blocks are caused because the underlying
AVURLAsset
's properties are lazy-loaded, and if you don't load them yourself, they get busy-loaded on the main thread when theAVPlayer
wants to play.Check out the AVAsset documentation, especially the stuff around
AVAsynchronousKeyValueLoading
. I think we needed to load the values forduration
andtracks
before using the asset on anAVPlayer
to minimize the main thread blocks. It's possible we also had to walk through each of the tracks and doAVAsynchronousKeyValueLoading
on each of the segments, but I don't remember 100%.