Swift中AVPlayer视频加载显示缩略图,播放后自动隐藏问题求助
问题:视频播放后缩略图无法隐藏
在UITableView单元格中实现了视频自动播放,期望视频加载完成前显示缩略图,但视频开始播放后缩略图无法隐藏。使用AVPlayer,单元格显示时调用.play()启动视频,理论上此时应设置缩略图.isHidden = true,但实际未生效。
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() } } }
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() } }
修复方案
问题根源
willDisplay中判断isPlaying时,视频播放是异步操作,isPlaying状态更新滞后,导致提前判断时状态未改变。- 未建立播放状态与缩略图显示的动态绑定,无法在视频真正开始播放时及时触发缩略图隐藏。
- 单元格复用未重置缩略图状态,可能导致旧状态残留。
具体修改步骤
1. 给PlayerView添加播放状态回调
在PlayerView类中添加闭包属性,用于通知外部播放状态变化:
// PlayerView.swift 中添加 var onPlayStateChanged: ((Bool) -> Void)?
修改play()、pause()和startLoading()方法,触发回调:
func play() { guard self.assetPlayer?.isPlaying == false else { return } DispatchQueue.main.async { self.assetPlayer?.play() self.isPlaying = true self.onPlayStateChanged?(true) // 通知播放开始 } } func pause() { guard self.assetPlayer?.isPlaying == true else { return } DispatchQueue.main.async { self.assetPlayer?.pause() self.isPlaying = false self.onPlayStateChanged?(false) // 通知播放暂停 } } 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() self.isPlaying = true self.onPlayStateChanged?(true) // 自动播放时触发回调 } } } }
2. 在Cell中绑定回调控制缩略图
在创建Cell的代码中,给playerView绑定回调,动态控制缩略图显示:
if let playerView = videoCell.playerView { // ... 原有约束代码 ... // 绑定播放状态回调 playerView.onPlayStateChanged = { isPlaying in videoCell.thumbnailImageView?.isHidden = isPlaying } playerView.play() }
3. 修正TableView的willDisplay方法
移除willDisplay中的状态判断,仅保留播放触发:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if let videoCell = cell as? HomeVideoCell { videoCell.playerView?.play() } }
4. 处理Cell复用的状态重置
给HomeVideoCell添加prepareForReuse方法,重置缩略图和播放状态:
class HomeVideoCell: UITableViewCell { // ... 现有属性 ... override func prepareForReuse() { super.prepareForReuse() thumbnailImageView?.isHidden = false playerView?.pause() } }
调试建议
- 添加打印日志,在
onPlayStateChanged回调中输出isPlaying值,确认状态是否正确触发。 - 检查
thumbnailImageView是否被正确添加到视图层级,且层级在playerView下方(否则视频播放后仍被遮挡)。 - 确认
playerView的loaded状态是否正确,避免未加载完成就调用play()。
内容的提问来源于stack exchange,提问作者Zach Handley
相关产品推荐
相关产品推荐

