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