For God’s sake, can you autoplay video in list? – iOS


After following the last article, my friends were very anxious just like you are! Just a day ago this friend calls me up, “Is it done? For God’s sake, Can you autoplay video in list?”. Jeez, calm down already and follow through to learn how to autoplay video in list in iOS!

Note: If you haven’t already read the previous article, read it here.

Getting pumped up

In the last article, we created our own PlayerView and played a video, all working good so far!

Now for our final destination, I think we need a list. A collection view? Yes, you guessed it right!

If we create the list first, what would we show as a list item?

Good catch, let’s build the list item, a collection view cell first!

List item – Collection View Cell

To keep it simple, we will just have a list item with only a PlayerView, nothing else!

Here’s our VideoCollectionViewCell:

class VideoCollectionViewCell: UICollectionViewCell {
     
     let containerView:UIView = {
         let view = UIView()
         view.backgroundColor = .white
         view.translatesAutoresizingMaskIntoConstraints = false
         return view
     }()
     
     let playerView:PlayerView = {
         let view = PlayerView()
         view.clipsToBounds = true
         view.translatesAutoresizingMaskIntoConstraints = false
         return view
     }()
     
     override func awakeFromNib() {
         super.awakeFromNib()
         self.setUpUI()
     }
     
     override init(frame: CGRect) {
         super.init(frame: frame)
         self.setUpUI()
     }
     
     required init?(coder: NSCoder) {
         super.init(coder: coder)
         self.setUpUI()
     }
     
     func setUpUI() {
         
         self.addSubview(containerView)
         
         containerView.applyConstraints(.fitInView(self))
         
         self.containerView.addSubview(playerView)
         
         playerView.applyConstraints(.fitInView(self.containerView))
         
     }
     
 }

See? Simple as I said! We just created a PlayerView like before and added it in our cell. Not complicated at all!

Awesome! It’s time to add some more code to make it usable:

 //..
 var url: URL?

 func play() {
     if let url = url {
         playerView.prepareToPlay(withUrl: url)
     }
 }

 func pause() {
     playerView.pause()
 }

 func configure(_ videoUrl: String) {
     guard let url = URL(string: videoUrl) else { return }
     self.url = url
     playerView.prepareToPlay(withUrl: url)
 }
 //..

Quick run through? Sure.

We added play() and pause() helper functions to be consumed by the list.

We also added configure function, again, to be consumed by the list to set the video url and configure the player view.

Alright, we have a list item. Let’s hit the road and create our list.

List – Collection View

Head up to our ViewController that we created in last article.

Replace the PlayerView with a UICollectionView:

class ViewController: UIViewController {
     
     let containerView:UIView = {
         let view = UIView()
         return view
     }()
     
     lazy var collectionView:UICollectionView = {
         let layout = UICollectionViewFlowLayout.init()
         layout.scrollDirection = .vertical
         layout.minimumLineSpacing = 10
         layout.minimumInteritemSpacing = 0
         layout.footerReferenceSize = .zero
         layout.headerReferenceSize = .zero
         let collectionView = UICollectionView.init(frame: .zero, collectionViewLayout: layout)
         collectionView.isPagingEnabled = false
         collectionView.isScrollEnabled = true
         collectionView.backgroundColor = .white
         collectionView.showsVerticalScrollIndicator = true
         collectionView.showsHorizontalScrollIndicator = false
         collectionView.register(VideoCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
         collectionView.delegate = self
         collectionView.dataSource = self
         return collectionView
     }()
     
     override func viewDidLoad() {
         super.viewDidLoad()
         
         self.setUpUI()
         
     }
     
     func setUpUI() {
         
         self.view.backgroundColor = .white
         
         self.view.addSubview(containerView)
         
         containerView.applyConstraints(.fitInSafeArea(self.view.safeAreaLayoutGuide))
         
         containerView.addSubview(collectionView)
         
         collectionView.applyConstraints(.fitInView(containerView))
         
     }
     
 }

Before we go ahead, first create a data source to populate the list:

 var dataSource:[String] = [] 

 func setUpDataSource() {
     dataSource = [
         "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
         "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
         "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
         "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
         "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4",
         "https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4"
     ]
     collectionView.reloadData()
 }

Call this function from the from viewDidLoad right after setUpUI() call.

Since we have our data source, lets implement collection view delegate functions:

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
     
     func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
         return dataSource.count
     }
     
     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
         guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? VideoCollectionViewCell else {
             return UICollectionViewCell()
         }
         cell.configure(dataSource[indexPath.item])
         return cell
     }
     
     func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
         return CGSize(width: collectionView.frame.width, height: 240)
     }
     
 }

We dequeue cell and configure it using the data source we created earlier. Pretty much self explanatory right?

Run the app and see the progress we have made so far.

You will see that all the videos load and instantly start playing and stops once finished playing.

If you scroll down, the same happens to these list items.

This does not look good, does it? Our goal is to play a single video at a time.

To resolve this, we will only play a video if the list item is fully visible on the screen and is the first visible item.

Let’s first prepare our PlayerView to handle this.

PlayerView 2.0

The PlayerView we created is perfect already, we don’t need to change it entirely.

 private var url: URL?
 
 func prepareToPlay(withUrl url:URL, shouldPlayImmediately: Bool = false) {
     // 1.
     guard !(self.url == url && assetPlayer != nil && assetPlayer?.error == nil) else {
         if shouldPlayImmediately {
             play()
         }
         return
     }
     
     // 2.
     cleanUp()
     
     self.url = url
     
     let options = [AVURLAssetPreferPreciseDurationAndTimingKey : true]
     let urlAsset = AVURLAsset(url: url, options: options)
     self.urlAsset = urlAsset
     
     let keys = ["tracks"]
     urlAsset.loadValuesAsynchronously(forKeys: keys, completionHandler: { [weak self] in
         guard let strongSelf = self else { return }
         // 3.
         strongSelf.startLoading(urlAsset, shouldPlayImmediately)
     })
 }

 private func startLoading(_ asset: AVURLAsset, _ shouldPlayImmediately: Bool) {
     var error:NSError?
     let status:AVKeyValueStatus = asset.statusOfValue(forKey: "tracks", error: &error)
     if status == AVKeyValueStatus.loaded {
         let item = AVPlayerItem(asset: asset)
         self.playerItem = item
         let player = AVPlayer(playerItem: item)
         self.assetPlayer = player
         // 4.
         if shouldPlayImmediately {
             DispatchQueue.main.async {
                 player.play()
             }
         }
     }
 }

Let’s go over this step by step:

  1. We check if the player is already loaded for the given url. If the player is loaded already and there’s no error, we’ll play the video directly based on the conditional variable, shouldPlayImmediately.
  2. If the player is not loaded with the correct url or the player had some error while playing the video, we will clean up the player and setup again.
  3. We pass-on the same condition to startLoading function.
  4. When the player is loaded, we check if it was supposed to play immediately. If yes, then only we play the video. Since we will have a multiple players loading, it’s good to play it on the main thread.

Great, you understood the code.

Now if you are wondering why we did so, let me help you understand, the condition was to control the player to play right away or wait for the consumer to play it later.

Let’s setup the direct consumer of our PlayerView, VideoCollectionViewCell.

VideoCollectionViewCell 2.0 already!

We are going to control the player, when to play immediately and when to just load the video.

Simple logic here is, when the cell is loaded we don’t need to autoplay it. So we will change our configure function.

We would play the video immediately when the consumer of the cell would command to play(), so we will change that as well.

 //..
 func play() {
     if let url = url {
         playerView.prepareToPlay(withUrl: url, shouldPlayImmediately: true)
     }
 }
 
 func configure(_ videoUrl: String) {
     guard let url = URL(string: videoUrl) else { return }
     self.url = url
     playerView.prepareToPlay(withUrl: url, shouldPlayImmediately: false)
 }
 //..

Awesome, we are done here!

Next up is our ViewController!

ViewController Revamp

I am sorry, but I just love to revamp things!

Our logic here is to find the first visible list item and play it.

Let’s hit the road:

 func playFirstVisibleVideo(_ shouldPlay:Bool = true) {
     // 1.
     let cells = collectionView.visibleCells.sorted {
         collectionView.indexPath(for: $0)?.item ?? 0 < collectionView.indexPath(for: $1)?.item ?? 0
     }
     // 2.
     let videoCells = cells.compactMap({ $0 as? VideoCollectionViewCell })
     if videoCells.count > 0 {
         // 3.
         let firstVisibileCell = videoCells.first(where: { checkVideoFrameVisibility(ofCell: $0) })
         // 4.
         for videoCell in videoCells {
             if shouldPlay && firstVisibileCell == videoCell {
                 videoCell.play()
             }
             else {
                 videoCell.pause()
             }
         }
     }
 }
 
 func checkVideoFrameVisibility(ofCell cell: VideoCollectionViewCell) -> Bool {
     // 5.
     var cellRect = cell.containerView.bounds
     cellRect = cell.containerView.convert(cell.containerView.bounds, to: collectionView.superview)
     return collectionView.frame.contains(cellRect)
 }

Step by step walkthrough:

  1. We get the visible cells of the collection view, sort it according to indexPath. (Yes, the cells are not in sorted order already! Bummer!)
  2. Since these cells are UICollectionViewCell, we map them as VideoCollectionViewCell.
  3. We get the first visible cell by checking the list with our own function which checks if the cell is fully visible.
  4. We loop through the visible cells and play the first visible cell based on the conditional variable, shouldPlay, and pause rest videos.
  5. The function, checkVideoFrameVisibility, gets the list item’s bounds and converts it with respect to our ViewController. Finally checks if collection view’s frame contains the whole cellRect.

Awesome! We are ready with our setup!

What would be the right place to call this function, playFirstVisibleVideo? The answer is viewDidAppear!

override func viewDidAppear(_ animated: Bool) {
     super.viewDidAppear(animated)
     self.playFirstVisibleVideo()
 }

override func viewWillDisappear(_ animated: Bool) {
     super.viewWillDisappear(animated)
     self.playFirstVisibleVideo(false)
 }

Great! We would also want to pause the playing video if the ViewController is not there on the screen anymore, right? So we do that in viewWillDisappear.

Run the app and see how it works!

autoplay video in list in iOS

As you can see, the first video starts playing as soon as it’s loaded and stops.

Nothing happens when you scroll the list! That’s not what we want!

Missing piece of the puzzle – UIScrollViewDelegate

You want something to do with scrolling? UIScrollViewDelegate is here to save you!

Without furher ado, let’s add it:

extension ViewController: UIScrollViewDelegate {
     
     func scrollViewDidScroll(_ scrollView: UIScrollView) {
         playFirstVisibleVideo()
     }
     
 }

Just that, nothing more!

What this will do is, whenever the list is scrolled this will call our dear function, playFirstVisibleVideo, which will play the first visible video! Superb!

Don’t hesitate, go ahead and run the app now! The first video plays and stops, you scroll the list and the next video starts playing! Awesome!

You have learnt how to autoplay video in list in iOS!

Before I leave you, there’s a bonus!

Bonus: Play video in loop

Notice what happens when you play the full video once and scroll down, the second video starts playing. When you scroll back up, the first video would not play!

We should do something when the video ends, maybe play it again. So technically we’ll be playing video in loop!

 func prepareToPlay(withUrl url:URL, shouldPlayImmediately: Bool = false) {
     //..
     NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
 }
 
 func cleanUp() {
     //..
     NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
 }
 
 @objc private func playerItemDidReachEnd(_ notification: Notification) {
     guard notification.object as? AVPlayerItem == self.playerItem else { return }
     DispatchQueue.main.async {
         guard let videoPlayer = self.assetPlayer else { return }
         videoPlayer.seek(to: .zero)
         videoPlayer.play()
     }
 }

We can add an observer for the notification, NSNotification.Name.AVPlayerItemDidPlayToEndTime, which gets fired when a video has played to the end.

We’d remove the observer in during the clean up, we are good devs!

When we receive the notification, we’d check if the current playerItem is the one which finished playing the video.

If so, we’d seek the video back to start and play it, easy?

Run the app now, and you’d be glad we did this bonus content!

autoplay video in list in ios
Autoplay video on scroll

Woohoo! We did it! Congratulations!

This is all you need to know to autoplay video in a list in iOS.

Ending notes – Some Self Learnings

If you see, in the current implementation, the last video would not get played, ever!

Why?? Because it will never be the first visible video! So Best practice here is to have a cell height such that only one video can be fully visible in the screen at a time.

Having less videos, makes it easier to load and play the videos as well.

You can add more features to this, such as mute/unmute the videos and maintain it for all the videos how Instagram does it.

You can add a max replay count to control how many times a video would be re-played in a loop, and on reaching the max count you can show a play button.

Thank you for reading it through! I hope this helps you out to learn how to autoplay video in list in iOS.

Feel free to leave a comment if you need any more help.

Find the full sourcecode of this article on GitHub:

https://github.com/mobiraft/AutoPlayVideoInListExample


Checkout this trending article on How To Add Splash Screen in SwiftUI.

If you want to learn how to use SwiftUI in your existing app, then you can read it here!

or If you want to learn how to use and convert your UIViewControllers in SwiftUI, then you can read it here!


Like it? Share with your friends!

3 Comments

Your email address will not be published. Required fields are marked *

  1. Hi,
    suppose I have record a video of 15 seconds using ImagePickerController then, how can I play and pause that video ?

    1. Hey Hiren,
      I am not very sure about your entire usecase. From what I understand, If you have a video that you want to play and pause, you can maintain the state of the video, playing/paused, and to change the states you can add a button on the UI that would call the play() or pause() functions of Player & change the state accordingly.
      Feel free to elaborate more if this does not answer your question. Drop me an email on hardik@mobiraft.com.
      Best,
      Hardik.