I wrote a UIView subclass "VideoPlayerView" to encapsulate AVFoundation video playback. I believed that I had a bulletproof KVO pattern set up to handle observation of the AVPlayer, AVPlayerItems and AVURLAssets for the purpose of loading, playback and error handling.
Instead, I find crashes being reported that this pattern was specifically set up to guard against (rarely, but reported nonetheless).
a) An instance 0x170019730 of class AVPlayerItem was deallocated while key value observers were still registered with it.
b) [VideoPlayerView setPlayerItem:] Cannot remove observer VideoPlayerView for the key path "status" from AVPlayerItem because it is not registered as an observer.
c) [VideoPlayerView setAsset:] Cannot remove an observer VideoPlayerView 0x145e3bbd0 for the key path "playable" from AVURLAsset 0x170233780 because it is not registered as an observer.
I would like to learn why these errors are occurring, what it is that I have missed or misunderstood and how to make things more robust.
Specific details are simplified for the purpose of the explanation, but I believe all relevant information is here.
I have a class VideoPlayerView, it holds these properties amongst others:
@property (strong, nonatomic) AVPlayerItem *playerItem;
@property (strong, nonatomic) AVURLAsset *asset;
@property (strong, nonatomic, readonly) AVPlayerLayer *playerLayer;
Note that all references are strong - these objects cannot be deallocated until the VideoPlayerView (which is doing the observing) is itself deallocated. AVPlayerLayer maintains a strong reference to its AVPlayer property.
I implement custom getters as follows:
- (AVPlayer*)player
{
return [(AVPlayerLayer*)self.layer player];
}
- (AVPlayerLayer *)playerLayer
{
return (AVPlayerLayer *)self.layer;
}
I implement custom setters as follows:
- (void) setPlayer:(AVPlayer*)player
{
// Remove observation for any existing player
AVPlayer *oldPlayer = [self player];
[oldPlayer removeObserver:self forKeyPath:kStatus];
[oldPlayer removeObserver:self forKeyPath:kCurrentItem];
// Set strong player reference
[(AVPlayerLayer*)[self layer] setPlayer:player];
// Add observation for new player
[player addObserver:self forKeyPath:kStatus options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
[player addObserver:self forKeyPath:kCurrentItem options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
}
- (void) setAsset:(AVURLAsset *)asset
{
// Remove observation for any existing asset
[_asset removeObserver:self forKeyPath:kPlayable];
// Set strong asset reference
_asset = asset;
// Add observation for new asset
[_asset addObserver:self forKeyPath:kPlayable options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
}
- (void) setPlayerItem:(AVPlayerItem *)playerItem
{
// Remove observation for any existing item
[_playerItem removeObserver:self forKeyPath:kStatus];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[nc removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:_playerItem];
[nc removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:_playerItem];
// Set strong playerItem reference
_playerItem = playerItem;
// Add observation for new item
[_playerItem addObserver:self forKeyPath:kStatus options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
if (_playerItem)
{
[nc addObserver:self selector:@selector(handlePlayerItemDidReachEndTimeNotification:) name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[nc addObserver:self selector:@selector(handlePlayerItemFailureNotification:) name:AVPlayerItemPlaybackStalledNotification object:_playerItem];
[nc addObserver:self selector:@selector(handlePlayerItemFailureNotification:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:_playerItem];
}
}
Outside of these custom setters, VideoPlayerView always uses "self.property =" or "[self setProperty:]" and never "_property =", so that the custom setter is always used.
Finally, VideoPlayerView implements a dealloc method as follows:
- (void) dealloc
{
[self releasePlayerAndAssets];
}
- (void) releasePlayerAndAssets
{
[self setAsset:nil];
[self setPlayerItem:nil];
[self setPlayer:nil];
}
Yes, I should just inline this pointless abstraction! Nevertheless, this means that on deallocation of the VideoPlayerView, any strong properties therein have their observation removed, and are then released to allow their deallocation.
So then, I believe this pattern should mitigate the crashes I am observing as follows:
a) An instance 0x170019730 of class AVPlayerItem was deallocated while key value observers were still registered with it.
VideoPlayerView is the only class of mine observing AVPlayerItem. VideoPlayerView maintains a strong reference to AVPlayerItem all the while that it is observing it. Therefore AVPlayerItem cannot be deallocated while VideoPlayerView is alive, and prior to its deallocation VideoPlayerView will stop observing the AVPlayerItem prior to AVPlayerItem's subsequent deallocation.
How is this going wrong?
b) [VideoPlayerView setPlayerItem:] Cannot remove observer VideoPlayerView for the key path "status" from AVPlayerItem because it is not registered as an observer.
c) [VideoPlayerView setAsset:] Cannot remove an observer VideoPlayerView 0x145e3bbd0 for the key path "playable" from AVURLAsset 0x170233780 because it is not registered as an observer.
My custom setters are trying to remove the observation of any previously set AVPlayerItem or AVURLAsset prior to replacing the property with the pointer to the new or incoming AVPlayerItem or AVURLAsset.
When my class is instantiated, _playerItem and _asset are nil. Therefore any previous AVPlayerItem or AVURLAsset must have been set through the custom setter and therefore have VideoPlayerView registered as an observer for those keypaths.
How are these properties being set without observation being set up?
Are these just horrible race conditions based on the order of method calls in the custom setters?
Is there something fundamental I am missing here?
I am considering using the objective-c runtime to create an associated object property BOOL isObserved on these objects just to be able to do a sanity check before trying to remove observer. I get the feeling even this won't be robust enough given the issues with the current methodology.
Any insight or help much appreciated. Thank you for reading.