AVPlayer issue in Swift

66 Views Asked by At

I have few crashes on my iOS app related to AVPlayer which they are occurring randomly and they cannot be reproduced. The information about the crashes is the following:

"NSInternalInconsistencyException Cannot update for observer <NSKeyValueObservance 0x280b56f10> for the key path "currentItem.hasEnabledVideo" from <MediaPlayer 0x280b030f0>, most likely because the value for the key "currentItem" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the MediaPlayer class."

"NSInternalInconsistencyException Cannot remove an observer <NSKeyValueObservance 0x303f4b570> for the key path "currentItem.status" from <MediaPlayer 0x3020fb5a0>, most likely because the value for the key "currentItem" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the MediaPlayer class."

Also there are similar messages about other key paths of "currentItem".

I have the following implementation of AVPlayer in Swift. The line of code that causes the crashes is

super.replaceCurrentItem(with: nil) , which is inside the "prepareForRelease()" function.

import Foundation
import Combine
import AVFoundation
import ApplicationCore

@objc
public class MediaPlayer: AVPlayer, ObservableObject {
    
    private enum Command: Equatable {
        case play
        case pause
        case replaceItem(AVPlayerItem?)
        
        var identity: AnyHashable {
            switch self {
            case .play:
                return 0
            case .pause:
                return 1
            case .replaceItem(let playerItem):
                return HashSignature(2, playerItem?.assetURL)
            }
        }
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.identity == rhs.identity
        }
    }
    
    // MARK: Factory Methods
    
    public static func make() -> MediaPlayer {
        let instance = MediaPlayer()
        instance.autowire()
        
        return instance
    }
    
    public static func make(url `URL`: URL) -> MediaPlayer {
        let instance = MediaPlayer(url: `URL`)
        instance.autowire()
        
        return instance
    }
    
    public static func make(playerItem item: AVPlayerItem?) -> MediaPlayer {
        let instance = MediaPlayer(playerItem: item)
        instance.autowire()
        
        return instance
    }
    
    // MARK: Initialization
    
    @Atomic
    private var currentItemURL: URL?
    
    private let commandSubject = PassthroughSubject<Command, Never>()
    
    private var subscriptions: Set<AnyCancellable> = []
    private var itemSubscriptions: Set<AnyCancellable> = []
    
    public override var currentItem: AVPlayerItem? {
        set {
            replaceCurrentItem(with: newValue)
        }
        
        get {
            super.currentItem
        }
    }
    
    private func autowire() {
        subscriptions.cancelAll()
        
        publisher(for: \.status)
            .replaceWithVoid()
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
            .store(in: &subscriptions)
        
        publisher(for: \.error)
            .replaceWithVoid()
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
            .store(in: &subscriptions)
        
        publisher(for: \.timeControlStatus)
            .replaceWithVoid()
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
            .store(in: &subscriptions)
        
        publisher(for: \.reasonForWaitingToPlay)
            .replaceWithVoid()
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
            .store(in: &subscriptions)
        
        commandSubject
            .removeDuplicates()
            .queued()
            .sink { [weak self] command in
                self?.handleCommand(command)
            }
            .store(in: &subscriptions)
    }
    
    deinit {
        dismantle()
    }
    
    public func dismantle() {
        subscriptions.cancelAll()
        itemSubscriptions.cancelAll()
    }
    
    public override func play() {
        commandSubject.send(.play)
    }
    
    public override func playImmediately(atRate rate: Float) {
        commandSubject.send(.play)
    }
    
    public override func pause() {
        commandSubject.send(.pause)
    }
    
    public override func replaceCurrentItem(with item: AVPlayerItem?) {
        commandSubject.send(.replaceItem(item))
    }
    
    public func prepareForRelease() {
        dismantle()
        
        guard currentItem != nil else {
          return
        }
         
        super.pause()
        super.replaceCurrentItem(with: nil)
    }
    
    public func remediateInvalidPlayingState() {
        super.pause()
        commandSubject.send(.pause)
    }
    
    // MARK: Commands
    
    private func handleCommand(_ command: Command) {
        
        guard subscriptions.isNotEmpty else {
            return
        }
        
        switch command {
        case .play:
            handlePlayCommand()
        case .pause:
            handlePauseCommand()
        case .replaceItem(let item):
            handleReplaceItemCommand(item)
        }
    }
    
    private func handlePlayCommand() {
        super.play()
    }
    
    private func handlePauseCommand() {
        super.pause()
    }
    
    private func handleReplaceItemCommand(_ item: AVPlayerItem?) {
        itemSubscriptions.cancelAll()
        
        currentItemURL = self.assetURL(from: item)
        super.replaceCurrentItem(with: item)
        
        objectWillChange.send()
        
        guard item != nil else {
            return
        }
        
        makeCurrentItemObservations(for: item)
    }
    
    private func makeCurrentItemObservations(for currentItem: AVPlayerItem?) {
        itemSubscriptions.cancelAll()
        
        guard let currentItem = currentItem else {
            return
        }
        
        currentItem.publisher(for: \.status)
            .replaceWithVoid()
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
            .store(in: &itemSubscriptions)
        
        currentItem.publisher(for: \.isPlaybackLikelyToKeepUp)
            .replaceWithVoid()
            .sink { [weak self] in
                self?.objectWillChange.send()
            }
            .store(in: &itemSubscriptions)
    }
    
    public func isPlaying<Media: RemoteMedia>(item media: Media?) -> Bool {
        return currentItemURL == media?.mediaURI
    }
    
    public func isPlaying(item playerItem: AVPlayerItem?) -> Bool {
        return assetURL(from: playerItem) == currentItemURL
    }
    
    private func assetURL(from playerItem: AVPlayerItem?) -> URL? {
        return playerItem?.assetURL
    }
    
    // MARK: KVO
    
    public override func setValue(_ value: Any?, forKey key: String) {
        guard key == "currentItem" else {
            super.setValue(value, forKey: key)
            return
        }
        
        replaceCurrentItem(with: value as? AVPlayerItem)
    }
    
    public override func setValue(_ value: Any?, forKeyPath keyPath: String) {
        guard keyPath == "currentItem" else {
            super.setValue(value, forKeyPath: keyPath)
            return
        }
        
        replaceCurrentItem(with: value as? AVPlayerItem)
    }
    
    public override func setNilValueForKey(_ key: String) {
        guard key == "currentItem" else {
            super.setNilValueForKey(key)
            return
        }
        
        replaceCurrentItem(with: nil)
    }
}

extension AVPlayerItem {
    
    var assetURL: URL? {
        guard let urlAsset = self.asset as? AVURLAsset else {
            return nil
        }
        
        return urlAsset.url
    }
}
0

There are 0 best solutions below