【问题标题】:Wrong reference to custom AVPlayer对自定义 AVPlayer 的错误引用
【发布时间】:2020-04-01 04:00:47
【问题描述】:

我有一个带有多个单元格的UICollectionView (FeedCell)。这些单元格还可以包含一个UICollectionView,它也可以有多个单元格,最多五个(MediaSliderCell)。基本上,设置与 Instagram 相同:您有一个帖子,一个帖子可以有多个图像或视频。

我现在面临的问题是,有时在错误的单元格中显示了错误的视频。我使用 imageView 在视频尚未播放时显示占位符,当视频开始播放(点击播放按钮)时,此占位符被隐藏。

我认为当视频不播放时一切正常,但当我播放视频时出现问题。单元格被切换,这意味着MediaSliderCell indexPath.item 5 的视频显示在MediaSLiderCell indexPath.item 2 中。

起初,我认为问题出在单元格上,不能很好地重复使用,但这也意味着照片可能会被切换,但从未发生过。所以我觉得问题出在我的 AVPlayer 上,然后它使用了错误的引用或错误的 URL。让我演示一下我的代码:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaSliderCell", for: indexPath) as! MediaSliderCell
    cell.layer.shouldRasterize = true
    cell.layer.rasterizationScale = UIScreen.main.scale
        if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia!.count > 0) {
            if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaType == .photo) {
                    let mediaURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaURL)")
                    cell.photoView.kf.setImage(with: mediaURL, options: [.targetCache(mediaCache)])
                    cell.photoView.isHidden = false
                    cell.videoView.isHidden = true
            } else if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaType == .video) {
                let mediaThumbURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaThumbURL!)")
                let mediaURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaURL)")!
                cell.videoView.placeholderView.kf.setImage(with: mediaThumbURL, options: [.targetCache(mediaCache)])
                cell.videoView.mediaURL = mediaURL
                cell.photoView.isHidden = true
                cell.videoView.isHidden = false
            }
    }
    return cell
}

MediaSLiderCell 是一些非常基本的UICollectionViewCell 东西:

class MediaSliderCell: UICollectionViewCell {
    override func prepareForReuse() {
        super.prepareForReuse()
        photoView.isHidden = false
        videoView.isHidden = true
        videoView.mediaURL = nil
        videoView.placeholderView.kf.cancelDownloadTask()
        videoView.placeholderView.image = UIImage()
        photoView.image = UIImage()
    }

    var photoView: UIImageView = {
        let photoView = UIImageView()
        photoView.translatesAutoresizingMaskIntoConstraints = false
        photoView.backgroundColor = .black
        photoView.isHidden = true
        return photoView
    }()

    var videoView: VideoView = {
        let videoView = VideoView()
        videoView.translatesAutoresizingMaskIntoConstraints = false
        videoView.backgroundColor = .black
        return videoView
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    func setupViews() {
        addSubview(photoView)
        addSubview(videoView)
        photoView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        photoView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        photoView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        photoView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        videoView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        videoView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        videoView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        videoView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

我认为以上所有内容都很好。但是,对于 VideoView,我使用了自定义 UIView,在其中设置了占位符 (UIImageView)。下面有很多代码,但大多数是控件和用户界面的东西。重要的是要知道,我使用了两个库:CacheCachingPlayerItem。这是为了避免一遍又一遍地下载相同的视频,所以当视频在缓存中找不到时,我下载该项目,当视频下载时,我将其保存在缓存中以备后用。我猜一切都说得通。我觉得问题出在某个地方,或者在 AVPlayer 本身。看一下代码:

class VideoView: UIView, CachingPlayerItemDelegate {
    var playerItem: CachingPlayerItem?
    func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data) {
        // A track is downloaded. Saving it to the cache asynchronously.
        print("Saving video to cache on device")
        storage?.async.setObject(data, forKey: mediaURL.absoluteString, completion: { _ in} )
    }
    var playerLooper: NSObject?
    var playerLayer: AVPlayerLayer!
    var queuePlayer: AVQueuePlayer?
    var mediaURL: URL!
    var placeholderView: UIImageView = {
        let placeholderView = UIImageView()
        placeholderView.translatesAutoresizingMaskIntoConstraints = false
        placeholderView.backgroundColor = .black
        return placeholderView
    }()

    var playerView: UIView = {
        let playerView = UIView()
        playerView.translatesAutoresizingMaskIntoConstraints = false
        playerView.backgroundColor = .clear
        playerView.isHidden = true
        return playerView
    }()

    var playButton: UIImageView = {
        let playButton = UIImageView()
        playButton.image = UIImage(named: "playButton")
        playButton.translatesAutoresizingMaskIntoConstraints = false
        playButton.isUserInteractionEnabled = true
        playButton.backgroundColor = .clear
        return playButton
    }()

    var pauseButton: UIImageView = {
        let pauseButton = UIImageView()
        pauseButton.image = UIImage(named: "pauseButton")
        pauseButton.translatesAutoresizingMaskIntoConstraints = false
        pauseButton.backgroundColor = .clear
        pauseButton.isUserInteractionEnabled = true
        pauseButton.alpha = 0
        return pauseButton
    }()

    var volumeOnButton: UIImageView = {
        let volumeOnButton = UIImageView()
        volumeOnButton.image = UIImage(named: "volumeOn")
        volumeOnButton.translatesAutoresizingMaskIntoConstraints = false
        volumeOnButton.backgroundColor = .clear
        volumeOnButton.isUserInteractionEnabled = true
        volumeOnButton.alpha = 0
        return volumeOnButton
    }()

    var volumeOffButton: UIImageView = {
        let volumeOffButton = UIImageView()
        volumeOffButton.image = UIImage(named: "volumeOff")
        volumeOffButton.translatesAutoresizingMaskIntoConstraints = false
        volumeOffButton.backgroundColor = .clear
        volumeOffButton.isUserInteractionEnabled = true
        volumeOffButton.alpha = 0
        return volumeOffButton
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setupViews()
    }

    func setupViews() {
        addSubview(placeholderView)
        placeholderView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        placeholderView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        placeholderView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        placeholderView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        addSubview(playerView)
        playerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        playerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        playerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        playerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        let tapView = UITapGestureRecognizer(target: self, action: #selector(showControls))
        playerView.addGestureRecognizer(tapView)
        addSubview(playButton)
        playButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        playButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        playButton.heightAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
        playButton.widthAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
        let tapPlayButton = UITapGestureRecognizer(target: self, action: #selector(playVideo))
        playButton.addGestureRecognizer(tapPlayButton)
        addSubview(pauseButton)
        pauseButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        pauseButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        pauseButton.heightAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
        pauseButton.widthAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
        let tapPauseButton = UITapGestureRecognizer(target: self, action: #selector(pauseVideo))
        pauseButton.addGestureRecognizer(tapPauseButton)
        addSubview(volumeOnButton)
        volumeOnButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -(normalSpacing + 2)).isActive = true
        volumeOnButton.topAnchor.constraint(equalTo: topAnchor, constant: normalSpacing + 2).isActive = true
        volumeOnButton.heightAnchor.constraint(equalToConstant: 32).isActive = true
        volumeOnButton.widthAnchor.constraint(equalToConstant: 32).isActive = true
        let tapVolumeOnButton = UITapGestureRecognizer(target: self, action: #selector(volumeAction))
        volumeOnButton.addGestureRecognizer(tapVolumeOnButton)
        addSubview(volumeOffButton)
        volumeOffButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -(normalSpacing + 2)).isActive = true
        volumeOffButton.topAnchor.constraint(equalTo: topAnchor, constant: normalSpacing + 2).isActive = true
        volumeOffButton.heightAnchor.constraint(equalToConstant: 32).isActive = true
        volumeOffButton.widthAnchor.constraint(equalToConstant: 32).isActive = true
        let tapVolumeOffButton = UITapGestureRecognizer(target: self, action: #selector(volumeAction))
        volumeOffButton.addGestureRecognizer(tapVolumeOffButton)
    }

    @objc func volumeAction() {
        buttonTimer?.invalidate()
        volumeTimer?.invalidate()
        if UserDefaults.exists(key: "volumeOn") {
            if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
                self.queuePlayer?.isMuted = false
                UserDefaults.standard.set(true, forKey: "volumeOn")
                self.volumeOnButton.alpha = 1
                self.volumeOffButton.alpha = 0
            } else {
                self.queuePlayer?.isMuted = true
                UserDefaults.standard.set(false, forKey: "volumeOn")
                self.volumeOnButton.alpha = 0
                self.volumeOffButton.alpha = 1
            }
        } else {
            self.queuePlayer?.isMuted = false
            UserDefaults.standard.set(true, forKey: "volumeOn")
            self.volumeOnButton.alpha = 1
            self.volumeOffButton.alpha = 0
        }
        volumeTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
        buttonTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
    }

    @objc func checkVolume() {
        if UserDefaults.exists(key: "volumeOn") {
            if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
                self.queuePlayer?.isMuted = true
            } else {
                self.queuePlayer?.isMuted = false
            }
        } else {
            self.queuePlayer?.isMuted = true
        }
    }

    @objc func showControls() {
        buttonTimer?.invalidate()
        volumeTimer?.invalidate()
        if(self.volumeOnButton.alpha > 0 || self.volumeOffButton.alpha > 0) {
            UIView.animate(withDuration: 0.2) {
                self.volumeOnButton.alpha = 0
                self.volumeOffButton.alpha = 0
            }
        } else {
            if UserDefaults.exists(key: "volumeOn") {
                if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
                    UIView.animate(withDuration: 0.2) {
                        self.volumeOffButton.alpha = 1
                        self.volumeOnButton.alpha = 0
                    }
                } else {
                    UIView.animate(withDuration: 0.2) {
                        self.volumeOffButton.alpha = 0
                        self.volumeOnButton.alpha = 1
                    }
                }
            } else {
                UIView.animate(withDuration: 0.2) {
                    self.volumeOffButton.alpha = 1
                    self.volumeOnButton.alpha = 0
                }
            }
            volumeTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
        }

        if(self.queuePlayer?.timeControlStatus == .playing) {
            if(self.pauseButton.alpha > 0) {
                UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 0 }
            } else {
                UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 1 }
                buttonTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
            }
        } else if(self.queuePlayer?.timeControlStatus == .paused) {
            if(self.playButton.alpha > 0) {
                UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 0 }
            } else {
                UIView.animate(withDuration: 0.2) { self.playButton.alpha = 1 }
            }
        }
    }

    lazy var storage: Cache.Storage? = {
        return try? Storage(diskConfig: diskConfig, memoryConfig: memoryConfig, transformer: TransformerFactory.forData())
    }()

    func loadVideo() {
        // Trying to retrieve a track from cache asynchronously.
        storage?.async.entry(forKey: mediaURL.absoluteString, completion: { result in
            switch result {
            case .error:
                // The track is not cached.
                print("Downloading from network")
                self.playerItem = CachingPlayerItem(url: self.mediaURL)
            case .value(let entry):
                // The track is cached.
                print("Downloading from cached library on device")
                self.playerItem = CachingPlayerItem(data: entry.object, mimeType: "video/mp4", fileExtension: "mp4")
            }
            self.playerItem?.delegate = self
            DispatchQueue.main.async {
                if let playerItem = self.playerItem {
                    self.queuePlayer = AVQueuePlayer(items: [playerItem])
                    self.queuePlayer?.automaticallyWaitsToMinimizeStalling = false
                    self.playerLayer = AVPlayerLayer(player: self.queuePlayer)
                    self.playerLooper = AVPlayerLooper(player: self.queuePlayer!, templateItem: playerItem)
                    self.playerView.layer.addSublayer(self.playerLayer!)
                    self.playerLayer?.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 1.25)
                    self.checkVolume()
                    do {
                       try AVAudioSession.sharedInstance().setCategory(.playback)
                    } catch(let error) {
                        print(error.localizedDescription)
                    }
                    self.queuePlayer?.play()
                    self.queuePlayer?.addObserver(self, forKeyPath: "timeControlStatus", options: .initial, context:nil)
                }
            }
        })
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if(keyPath == "timeControlStatus") {
            if(self.playerView.isHidden && self.queuePlayer?.timeControlStatus == .playing) {
                self.playerView.isHidden = false
            }
        }
    }

    var buttonTimer: Timer?
    var volumeTimer: Timer?

    @objc func playVideo() {
        buttonTimer?.invalidate()
        volumeTimer?.invalidate()
        if(self.queuePlayer?.currentItem == nil) {
            self.loadVideo()
        }
        if(self.queuePlayer?.timeControlStatus == .paused) {
            do {
               try AVAudioSession.sharedInstance().setCategory(.playback)
            } catch(let error) {
                print(error.localizedDescription)
            }
            self.queuePlayer?.play()
        }
        self.playButton.alpha = 0
        buttonTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
        volumeTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
    }

    @objc func pauseVideo() {
        buttonTimer?.invalidate()
        self.queuePlayer?.pause()
        self.playButton.alpha = 1
        self.pauseButton.alpha = 0
        self.volumeOnButton.alpha = 0
        self.volumeOffButton.alpha = 0
    }

    @objc func fadePauseButton() {
        UIView.animate(withDuration: 0.8) {
            self.pauseButton.alpha = 0
        }
    }

    @objc func fadeVolumeButton() {
        UIView.animate(withDuration: 0.8) {
            self.volumeOnButton.alpha = 0
            self.volumeOffButton.alpha = 0
        }
    }
}

在我看来,问题应该在loadVideo() 上面的某个地方。但是,我提供了自定义 UIView VideoView 的所有代码,以避免任何错误或其他重要部分。有谁能帮助我吗?会帮我很大的忙,因为我已经研究了好几个星期了。我已经尝试了所有关于出队的建议,我已经尝试了所有可以重用单元格的方法,......但一切都没有奏效。所以我猜应该在AVPlayerCache 库或CachingPlayerItem 库中找到解决方案。将不胜感激任何帮助或建议。提前谢谢你。

作为记录,我忘记了一些东西,但应该从代码中清楚:AVPlayer 没有显示,因此最初没有调用loadVideo()。仅当用户点击播放按钮 (playVideo()) 时。如果我也不播放视频,一切都会顺利进行。照片显示正确,占位符显示正确,但视频混淆我开始播放一个或多个视频(点击播放按钮,调用playVideo(),调用loadVideo()) .

编辑:好的,所以我想出了一些办法。当单元被重用时,视频本身似乎没有改变。我的意思是,在 loadVideo() 中,AVQueuePlayer() 被设置,这就是改变视频显示和播放的原因。我觉得我需要设置这个块:

    // Trying to retrieve a track from cache asynchronously.
    storage?.async.entry(forKey: mediaURL.absoluteString, completion: { result in

        switch result {
        case .error:
            // The track is not cached.
            print("Downloading from network")
            DispatchQueue.main.async {
            self.playerItem = CachingPlayerItem(url: self.mediaURL)
            }
        case .value(let entry):
            // The track is cached.
            print("Downloading from cached library on device")
            DispatchQueue.main.async {
            self.playerItem = CachingPlayerItem(data: entry.object, mimeType: "video/mp4", fileExtension: "mp4")
            }
        }

        self.playerItem?.delegate = self

        DispatchQueue.main.async {

            if let playerItem = self.playerItem {
                self.queuePlayer = AVQueuePlayer(items: [playerItem])
                self.queuePlayer?.automaticallyWaitsToMinimizeStalling = false
                self.playerLayer = AVPlayerLayer(player: self.queuePlayer)
                self.playerLooper = AVPlayerLooper(player: self.queuePlayer!, templateItem: playerItem)
                self.playerView.layer.addSublayer(self.playerLayer!)
                self.playerLayer?.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 1.25)
                self.checkVolume()
                do {
                   try AVAudioSession.sharedInstance().setCategory(.playback)
                } catch(let error) {
                    print(error.localizedDescription)
                }
                self.queuePlayer?.addObserver(self, forKeyPath: "timeControlStatus", options: .initial, context:nil)

            }

        }

    })

其他地方,在视频播放之前。因为只有在点击播放按钮时才会调用它,但随后会显示错误的视频。已尝试直接在单元格中调用 loadVideo(),但似乎无法解决问题。我觉得我越来越近了。有什么想法吗?

【问题讨论】:

    标签: ios swift uicollectionview uicollectionviewcell avplayer


    【解决方案1】:

    不要将AVQueuePlayer 存储在 tableView 单元格中。请记住,您可以保留的 AVPlayer 实例数量有上限。

    将玩家本身、他们的配置、开始、暂停以及您拥有的任何其他操作移动到UIViewController 中,存储您的UITableView

    当用户按下UITableViewCell 上的播放时,你会告诉UIViewControllerdelegate 开始播放,UIViewController 会做类似的事情:

    1. 取消之前的播放
    2. 用玩家物品填充AVQueuePlayer
    3. AVQueuePlayer 插入UITableViewCell
    4. 开始播放

    重用单元格时不要忘记从UITableViewCell 中删除AVQueuePlayer

    【讨论】:

    • 本周末将更改我的代码。两个问题: 1. 这会修复视频的混淆吗?当只有一个提要项出队时也会发生这种情况,因此只有一个 AVQueuePlayer。 2. 当两个包含 AVQueuePlayer 的提要项可见时会发生什么?他们都会出现吗?
    • 使用 AVPlayer 会是个好主意吗?我可以在牢房中启动这些吗?我只使用 AVQueuePlayer 自动重播视频。
    猜你喜欢
    • 2016-10-03
    • 1970-01-01
    • 2021-09-07
    • 2012-02-27
    • 2012-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多