Search code examples
iosswiftavplayervideo-thumbnails

Swift AVPlayer Thumbnail If Video is Loading, Removed Once it Starts Playing?


I have an autoplaying video in a table cell that I want to show a thumbnail for before it's loaded automatically, right now the thumbnail loads but it doesn't hide after the fact. I have used an AVPlayer and when the table cell displays it calls .play() which starts the video and it's supposed to .isHidden = true the thumbnail. I don't know how to fix it or debug it, because in theory it should work?

PlayerView

//
//  PlayerView.swift
//  Yacht Now
//
//  Created by Zach Handley on 3/11/23.
//  Copyright © 2023 CRTVDigital. All rights reserved.
//

import Foundation
import UIKit
import AVKit

class PlayerView: UIView {
    private var url: URL?
    private var urlAsset: AVURLAsset?
    private var playerItem: AVPlayerItem?
    var loaded: Bool = false
    var activityIndicator: UIActivityIndicatorView?
    var isPlaying = false
    
    private var assetPlayer:AVPlayer? {
        didSet {
            DispatchQueue.main.async {
                if let layer = self.layer as? AVPlayerLayer {
                    layer.player = self.assetPlayer
                }
            }
        }
    }
    
    override class var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
    
    init() {
        super.init(frame: .zero)
        initialSetup()
    }
    
    required init?(coder: NSCoder) {
        super.init(frame: .zero)
        initialSetup()
    }
    
    private func initialSetup() {
        if let layer = self.layer as? AVPlayerLayer {
            // Do any configuration
            layer.videoGravity = AVLayerVideoGravity.resizeAspect
        }
    }
    
    func prepareToPlay(withUrl url:URL, shouldPlayImmediately: Bool = false) {
        guard !(self.url == url && assetPlayer != nil && assetPlayer?.error == nil) else {
            if shouldPlayImmediately {
                play()
            }
            return
        }
        
        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 }
            strongSelf.startLoading(urlAsset, shouldPlayImmediately)
        })
        NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
    }
    
    private func startLoading(_ asset: AVURLAsset, _ shouldPlayImmediately: Bool = false) {
        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
            self.loaded = true
            print("LOADED")
            
            if shouldPlayImmediately {
                DispatchQueue.main.async {
                    player.play()
                }
            }
        }
    }
    
    func getThumbnailImageFromVideoUrl(url: URL, completion: @escaping ((_ image: UIImage?)->Void)) {
        DispatchQueue.global().async { //1
            let asset = AVAsset(url: url) //2
            let avAssetImageGenerator = AVAssetImageGenerator(asset: asset) //3
            avAssetImageGenerator.appliesPreferredTrackTransform = true //4
            let thumnailTime = CMTimeMake(value: 2, timescale: 1) //5
            do {
                let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumnailTime, actualTime: nil) //6
                let thumbNailImage = UIImage(cgImage: cgThumbImage) //7
                DispatchQueue.main.async { //8
                    completion(thumbNailImage) //9
                }
            } catch {
                print(error.localizedDescription) //10
                DispatchQueue.main.async {
                    completion(nil) //11
                }
            }
        }
    }
    
    func toggle() {
        if self.assetPlayer?.isPlaying == false {
            play()
        } else {
            pause()
        }
    }
    
    func play() {
        guard self.assetPlayer?.isPlaying == false else { return }
//        if self.loaded && self.activityIndicator != nil {
//            self.activityIndicator?.stopAnimating()
//        }
        DispatchQueue.main.async {
            self.assetPlayer?.play()
            // Remove the thumbnail image view
            self.isPlaying = true
        }
    }
    
    func pause() {
        guard self.assetPlayer?.isPlaying == true else { return }
        DispatchQueue.main.async {
            self.assetPlayer?.pause()
            self.isPlaying = false
        }
    }
    
    func cleanUp() {
        pause()
        urlAsset?.cancelLoading()
        urlAsset = nil
        assetPlayer = nil
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
    }
    
    deinit {
        cleanUp()
    }
    
    @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()
        }
    }
}

My TableView

default:
                // print("failed")
                var videoCell: HomeVideoCell
                if let cachedCell = videoCells[indexPath] {
                    videoCell = cachedCell
                } else {
                    videoCell = tableView.dequeueReusableCell(withIdentifier: "HomeVideoCell") as! HomeVideoCell
                    videoCell.selectionStyle = .none
                    videoCell.playerView = PlayerView()
                    videoCells[indexPath] = videoCell
                    print("Video cell found and created PlayerView")
                }
                //  videoCell.img_thumb.roundCorners([.topLeft, .bottomLeft], radius: 10)
                let str = self.bottomBannerData[indexPath.row]["video_thumb"]
                let videoStr = self.bottomBannerData[indexPath.row]["video"]

                print("strValue: \(String(describing: str)) -- videoStrValue: \(String(describing: videoStr))")
                if videoStr != "" {
                    if let videoUrl = URL(string: "\(imageURL)\(videoStr!)") {
                        videoCell.playerView?.prepareToPlay(withUrl: videoUrl, shouldPlayImmediately: true)
                        videoCell.thumbnailImageView = UIImageView()
                        videoCell.playerView?.getThumbnailImageFromVideoUrl(url: videoUrl) { thumbnailImage in
                            videoCell.thumbnailImageView.image = thumbnailImage
                        }
                        videoCell.contentView.addSubview(videoCell.thumbnailImageView)
                        videoCell.thumbnailImageView.translatesAutoresizingMaskIntoConstraints = false
                        videoCell.thumbnailImageView.centerXAnchor.constraint(equalTo: videoCell.img_thumb.centerXAnchor).isActive = true
                        videoCell.thumbnailImageView.centerYAnchor.constraint(equalTo: videoCell.img_thumb.centerYAnchor).isActive = true
                        videoCell.thumbnailImageView.heightAnchor.constraint(equalTo: videoCell.img_thumb.heightAnchor).isActive = true
                        videoCell.thumbnailImageView.widthAnchor.constraint(equalTo: videoCell.img_thumb.widthAnchor).isActive = true
                        if let playerView = videoCell.playerView {
                            playerView.frame = videoCell.img_thumb.bounds
                            videoCell.contentView.addSubview(playerView)
                            playerView.translatesAutoresizingMaskIntoConstraints = false
                            playerView.centerXAnchor.constraint(equalTo: videoCell.img_thumb.centerXAnchor).isActive = true
                            playerView.centerYAnchor.constraint(equalTo: videoCell.img_thumb.centerYAnchor).isActive = true
                            playerView.heightAnchor.constraint(equalTo: videoCell.img_thumb.heightAnchor).isActive = true
                            playerView.widthAnchor.constraint(equalTo: videoCell.img_thumb.widthAnchor).isActive = true
                            if let activityIndicator = videoCell.contentView.viewWithTag(690) as? UIActivityIndicatorView {
                                activityIndicator.startAnimating()
                                playerView.activityIndicator = activityIndicator
                            }
                            playerView.play()
                            print("VIDEO CELL Set PlayerView with URL \(videoUrl)")
                        }
                    }
                }

                print("VIDEO CELL DONE")
                return videoCell

            }
        }
    }

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if let videoCell = cell as? HomeVideoCell {
            videoCell.playerView?.play()
            if videoCell.playerView?.isPlaying == true {
                videoCell.thumbnailImageView?.isHidden = true
            }
        }
    }

    func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if let videoCell = cell as? HomeVideoCell {
            videoCell.playerView?.pause()
        }
    }

Solution

  • Inside your play function, you're using DispatchQueue.main.async before you set isPlaying. That means that isPlaying isn't set until the next run loop.

    But, look at the call site:

    videoCell.playerView?.play()
    if videoCell.playerView?.isPlaying == true {
      videoCell.thumbnailImageView?.isHidden = true
    }
    

    You're checking isPlaying immediately after calling play, but the next run loop hasn't occurred yet.

    You could just remove the check:

    videoCell.playerView?.play()
    videoCell.thumbnailImageView?.isHidden = true
    

    Or, you could also move the check to the next run loop (although I see little point in doing this):

    videoCell.playerView?.play()
    DispatchQueue.main.async {
      if videoCell.playerView?.isPlaying == true {
        videoCell.thumbnailImageView?.isHidden = true
      }
    }
    

    Yet another method would be using a callback function to set isHidden -- only call the callback once you actually do self.isPlaying = true