GnarlyDog
GnarlyDog

Reputation: 1247

AVPlayerItem initial timedMetadata not being observed (KVO)

I have a class that is handling an AVPlayer (and AVPlayerItem) that reports back state, time, and timedMetadata to a delegate.

Works well except that about 70-80% of the time, the initial timedMetadata is not "key value observed". However after the first instance of timedMetadata being missed, all other timedMetadata seems to be observed without issue.

As a temporary fix, I've started to embed dummy timedMetadata tags in the beginning of videos that do nothing but "kick the tires" so to speak and everything works fine after that. Yet this seems pretty kludgy. I suspect that either I'm setting up the AVPlayerItem and KVO in a sub-optimal manner OR there's just a bug here.

Any ideas on why this might be happening are greatly appreciated! Code below....

// CL: Define constants for the key-value observation contexts.
static const NSString *ItemStatusContext;
static const NSString *ItemMetadataContext;
static const NSString *ItemPlaybackForcastContext;


- (id)initWithURL:(NSURL *)url
{
    if (self = [super init]) {

        __weak TFPAVController *_self = self;

        AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
        NSString *tracksKey = @"tracks";

        [asset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:tracksKey] completionHandler:
         ^{
             dispatch_async(dispatch_get_main_queue(),
                            ^{
                                NSError *error = nil;
                                AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];

                                if (status == AVKeyValueStatusLoaded) {
                                    AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
                                    [item addObserver:_self forKeyPath:@"status" options:0 context:&ItemStatusContext];
                                    [item addObserver:_self forKeyPath:@"timedMetadata" options:0 context:&ItemMetadataContext];
                                    [item addObserver:_self forKeyPath:@"playbackLikelyToKeepUp" options:0 context:&ItemPlaybackForcastContext];

                                    [[NSNotificationCenter defaultCenter] addObserver:_self
                                                                             selector:@selector(playerItemDidReachEnd:)
                                                                                 name:AVPlayerItemDidPlayToEndTimeNotification
                                                                               object:item];

                                    AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
                                    _self.totalRunTime = CMTimeGetSeconds(item.duration);
                                    [_self.delegate avPlayerNeedsView:player];

                                    _self.playerItem = item;
                                    _self.player = player;
                                }
                                else {
                                    NSLog(@"The asset's tracks were not loaded: %@ // [%@ %@]",
                                          error.localizedDescription,
                                          NSStringFromClass([self class]),
                                          NSStringFromSelector(_cmd));
                                }

                                _self.playerObserver = [_self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, _FrameRate_) 
                                                                                                  queue:NULL
                                                                                             usingBlock: ^(CMTime time) {
                                                                                                 _self.currentVideoTime = CMTimeGetSeconds([_self.playerItem currentTime]);
                                                                                             }];
                            });
         }];
    }

    return self;
}
#pragma mark - KVO Response Methods
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context 
{    
        __weak TFPAVController *_self = self;

    if (context == &ItemStatusContext) {
        dispatch_async(dispatch_get_main_queue(),
                       ^{
                           if (((AVPlayerItem *)object).status == AVPlayerItemStatusReadyToPlay) {

                               [_self.delegate videoIsLoadedInPlayer:_self];
                           }
                       });
        return;
    }
    else if (context == &ItemMetadataContext) {
        dispatch_async(dispatch_get_main_queue(),
                       ^{
                           [_self checkMetaDataForPlayerItem: (AVPlayerItem *)object];
                       });
        return;
    }
    else if (context == &ItemPlaybackForcastContext) {
        dispatch_async(dispatch_get_main_queue(),
                       ^{
                           AVPlayerItem *playerItem = object;                           
                           if (CMTimeGetSeconds([playerItem currentTime]) <= 0) return;

                           NSDictionary *notificationDictionary = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:playerItem.playbackLikelyToKeepUp] 
                                                                                              forKey:kAVPlayerStateKey];

                           [[NSNotificationCenter defaultCenter] postNotificationName:kAVPlayerNotification 
                                                                               object:self 
                                                                             userInfo:notificationDictionary];
                        });
        return;
    }

    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

}

- (void)checkMetaDataForPlayerItem:(AVPlayerItem *)item
{
    NSMutableDictionary *metaDict = [NSMutableDictionary dictionary];

    // CL: make sure there's stuff there
    if (item.timedMetadata != nil && [item.timedMetadata count] > 0) {
        // CL: if there is, cycle through the items and create a Dictionary
        for (AVMetadataItem *metadata in item.timedMetadata) {
            [metaDict setObject:[metadata valueForKey:@"value"] forKey:[metadata valueForKey:@"key"]];
        }
        // CL: pass it to the delegate
        [self.delegate parseNewMetaData:[NSDictionary dictionaryWithDictionary:metaDict]];
    }
}

Upvotes: 4

Views: 2186

Answers (1)

damian
damian

Reputation: 3674

Ahhh, KVO. Probably one of Apple's all-time worst design decisions.

I guess it's no longer relevant, but at a guess the problem you're having is that sometimes the value you're trying to observe has already been assigned to the key when you get around to adding yourself as an observer, so your observer selector isn't called.

To avoid this you can add NSKeyValueObservingOptionInitial to the options when calling addObserver:forKeyPath:options:context:, and your observer method will be invoked immediately with the current value.

Upvotes: 1

Related Questions