You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

修复方案

问题根源

  1. willDisplay中判断isPlaying时,视频播放是异步操作,isPlaying状态更新滞后,导致提前判断时状态未改变。
  2. 未建立播放状态与缩略图显示的动态绑定,无法在视频真正开始播放时及时触发缩略图隐藏。
  3. 单元格复用未重置缩略图状态,可能导致旧状态残留。

具体修改步骤

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 20:02:05