0

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()
        }
    }
Zach Handley
  • 914
  • 1
  • 12
  • 25

1 Answers1

0

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

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • It did not work moving it to the next loop. but just removing the if statement worked. Won't this also remove it when it isn't loaded though too? That's why I wanted to use that if statement. For instance on cellular data it can take a minute to load, but technically the `.play()` would be called. – Zach Handley Mar 18 '23 at 17:31
  • "Won't this also remove it when it isn't loaded though too?" You can always add another boolean check to make sure that the correct conditions are hit. Or, use the callback suggestion that I gave at the end of the answer. In regards to a slow connection, you'd need to use something like this if you really want to see when it starts playing: https://stackoverflow.com/questions/40781738/how-to-detect-avplayer-actually-started-to-play-in-swift – jnpdx Mar 18 '23 at 17:40