SwiftUI UIViewRepresentable AVPlayer crashing due to "periodTimeObserver"

378 Views Asked by At

I have a SwiftUI application which has a carousel of videos. I'm using an AVPlayer with UIViewRepresentable and I'm creating the carousel with a ForEach loop of my custom UIViewRepresentable view. I want to have a "periodicTimeObserver" on the active AVPlayer, but it crashes and says

"An instance of AVPlayer cannot remove a time observer that was added by a different instance of AVPlayer SwiftUI"

My question is how can I remove the periodicTimeObserver of an AVPlayer inside of a UIViewRepresentable inside of a UIView, without causing the app to crash?

Here is my code:

ForEach(videosArray.indices, id: \.self) { i in
    let videoURL = videosArray[i]
                            
    ZStack {
        VStack {
            VideoView.init(viewModel: viewModel, videoURL: URL(string: videoURL)!, videoIndex: i)
        }
    }
}

struct VideoView: UIViewRepresentable {
    @ObservedObject var viewModel = viewModel.init()
    var videoURL:URL
    var previewLength:Double?
    var videoIndex: Int

    func makeUIView(context: Context) -> UIView { 
        return PlayerView.init(frame: .zero, url: videoURL, previewLength: previewLength ?? 6)
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        if videoIndex == viewModel.currentIndexSelected {
            if let playerView = uiView as? PlayerView {
                if !viewModel.isPlaying { 
                    playerView.pause()
                } else { 
                    playerView.play(customStartTime: viewModel.newStartTime, customEndTime: viewModel.newEndTime)
                }
            }
        } else {
            if let playerView = uiView as? PlayerView {
                playerView.pause()
            }
        }
    }
}

public class ViewModel: ObservableObject {
    @Published public var currentIndexSelected: Int = 0
    @Published public var isPlaying: Bool = true

    @Published public var newStartTime = 0.0
    @Published public var newEndTime = 30.0
}


class PlayerView: UIView {
    private let playerLayer = AVPlayerLayer()
    private var previewTimer:Timer?
    var previewLength:Double
    var player: AVPlayer?
    var timeObserver: Any? = nil

    init(frame: CGRect, url: URL, previewLength:Double) {
        self.previewLength = previewLength
        super.init(frame: frame)
     
        player = AVPlayer(url: url)
        player!.volume = 0
        player!.play()
    
        playerLayer.player = player
        playerLayer.videoGravity = .resizeAspectFill
        playerLayer.backgroundColor = UIColor.black.cgColor
    
        layer.addSublayer(playerLayer)
    }

    required init?(coder: NSCoder) {
        self.previewLength = 15
        super.init(coder: coder)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer.frame = bounds
    }

    func pause() {
        if let timeObserver = timeObserver {
            self.player?.removeTimeObserver(timeObserver)
        
            self.timeObserver = nil
        }
    
        player?.pause()
    }

    @objc func replayFinishedItem(noti: NSNotification) {
        print("REPLAY FINISHED NOTIIIII: \(noti)")
    
        if let timeDict = noti.object as? [String: Any], let startTime = timeDict["startTime"] as? Double, let endTime = timeDict["endTime"] as? Double/*, let player = timeDict["player"] as? AVPlayer, let observer = timeDict["timeObserver"]*/ {
            self.removeTheTimeObserver()
            self.play(customStartTime: startTime, customEndTime: endTime)
        }
    }

    @objc func removeTheTimeObserver() {
        print("ATTEMPT TO REMOVE IT!")
    
        if let timeObserver = timeObserver {
            self.player?.removeTimeObserver(timeObserver)
        
            self.timeObserver = nil
        }
    }

    func play(at playPosition: Double = 0.0, customStartTime: Double = 0.0, customEndTime: Double = 15.0) {
        var startTime = customStartTime
        var endTime = customEndTime
    
        if customStartTime > customEndTime {
            startTime = customEndTime
            endTime = customStartTime
        }
    
        if playPosition != 0.0 { 
            player?.seek(to: CMTime(seconds: playPosition, preferredTimescale: CMTimeScale(1)))
        } else {
            player?.seek(to: CMTime(seconds: startTime, preferredTimescale: CMTimeScale(1)))
        }
    
        player?.play()
    
        var timeDict: [String: Any] = ["startTime": startTime, "endTime": endTime]
    
        NotificationCenter.default.addObserver(self, selector: #selector(self.replayFinishedItem(noti:)), name: .customAVPlayerShouldReplayNotification, object: nil)
    
    
        self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: 100), queue: DispatchQueue.main, using: { [weak self] time in
            guard let strongSelf = self else {
                return
            }
        
            let currentTime = CMTimeGetSeconds(strongSelf.player!.currentTime())
        
            let currentTimeStr = String(currentTime)
        
            if let currentTimeDouble = Double(currentTimeStr) {
                let userDefaults = UserDefaults.standard
                userDefaults.set(currentTimeDouble, forKey: "currentTimeDouble")
            
                NotificationCenter.default.post(name: .currentTimeDouble, object: currentTimeDouble)
            
                if currentTimeDouble >= endTime {
                    if let timeObserver = strongSelf.timeObserver {
                        strongSelf.player?.removeTimeObserver(timeObserver)
                        strongSelf.timeObserver = nil
                    }
                 
                    strongSelf.player?.pause()
                    NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
                } else if let currentItem = strongSelf.player?.currentItem {
                    let seconds = currentItem.duration.seconds
                
                    if currentTimeDouble >= seconds {
                        if let timeObserver = strongSelf.timeObserver {
                            strongSelf.player?.removeTimeObserver(timeObserver)
                            strongSelf.timeObserver = nil
                        }
                    
                        NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
                    }
                }
            }
        })
    }
}
0

There are 0 best solutions below