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()
}
}
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