9

I am using Apple's document demo. Here is the link for demo: AVPlayerDemo

my application is crashed after following steps:
1) Play song
2) fast forward from seek bar.
3) Click on next, 4) fast forward from seek bar.
Here is my crash Log:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of AVPlayer cannot remove a time observer that was added by a different instance of AVPlayer.'

And Here is the code for music player:

- (NSTimeInterval) playableDuration
{
    //  use loadedTimeRanges to compute playableDuration.
    AVPlayerItem * item = player.currentItem;

    if (item.status == AVPlayerItemStatusReadyToPlay) {
        NSArray * timeRangeArray = item.loadedTimeRanges;

        CMTimeRange aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue];

        double startTime = CMTimeGetSeconds(aTimeRange.start);
        double loadedDuration = CMTimeGetSeconds(aTimeRange.duration);

        NSLog(@"get time range, its start is %f seconds, its duration is %f seconds.", startTime/60, loadedDuration/60);
        return (NSTimeInterval)(startTime + loadedDuration);
    }
    else
    {
        return(CMTimeGetSeconds(kCMTimeInvalid));
    }
}  
-(NSTimeInterval)currentItemPlayableDuration{

    //  use loadedTimeRanges to compute playableDuration.
    AVPlayerItem * item = player.currentItem;

    if (item.status == AVPlayerItemStatusReadyToPlay) {
        NSArray * timeRangeArray = item.loadedTimeRanges;

        CMTime currentTime = player.currentTime;

        __block CMTimeRange aTimeRange;

        [timeRangeArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

       aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue];

       if(CMTimeRangeContainsTime(aTimeRange, currentTime))
            *stop = YES;

        }];

        CMTime maxTime = CMTimeRangeGetEnd(aTimeRange);

        return CMTimeGetSeconds(maxTime);
    }
    else
    {
        return(CMTimeGetSeconds(kCMTimeInvalid));
    }
}  
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if(player!= nil)
    {
        [player.currentItem removeObserver:self forKeyPath:@"status"];
    }
    [player pause];
    player = nil;
    btnPlay.hidden=true;
    btnPause.hidden=false;
    selectedSongIndex = indexPath.row;
    url = [[NSURL alloc] initWithString:[arrURL objectAtIndex:indexPath.row]];
    [self setupAVPlayerForURL:url];
    //[player play];
    AVPlayerItem *item = player.currentItem;
    [item addObserver:self forKeyPath:@"timedMetadata" options:NSKeyValueObservingOptionInitial| NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld| NSKeyValueObservingOptionPrior context:nil];
    [player play];
    //[tableView deselectRowAtIndexPath:indexPath animated:YES];
}  #pragma mark - Player Methods
- (IBAction)btnBack_Click:(id)sender {
    int index = selectedSongIndex;
    if (index==0) {
        if(player!= nil)
        {
            [player.currentItem removeObserver:self forKeyPath:@"status"];
        }
        [player pause];
        player = nil;
        selectedSongIndex = [arrURL count]-1;
        url = [[NSURL alloc] initWithString:[arrURL objectAtIndex:selectedSongIndex]];
        [self setupAVPlayerForURL:url];
        AVPlayerItem *item = player.currentItem;
        [item addObserver:self forKeyPath:@"timedMetadata" options:NSKeyValueObservingOptionInitial| NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld| NSKeyValueObservingOptionPrior context:nil];
        [player play];
    }
    else if(index >= 0 && index < [arrURL count])
    {
        if(player!= nil)
        {
            [player.currentItem removeObserver:self forKeyPath:@"status"];
        }
        [player pause];
        player = nil;
        index = selectedSongIndex - 1;
        selectedSongIndex = index;
        url = [[NSURL alloc] initWithString:[arrURL objectAtIndex:index]];
        [self setupAVPlayerForURL:url];
        AVPlayerItem *item = player.currentItem;
        [item addObserver:self forKeyPath:@"timedMetadata" options:NSKeyValueObservingOptionInitial| NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld| NSKeyValueObservingOptionPrior context:nil];
        [player play];
    }
}
- (IBAction)btnPlay_Click:(id)sender {

    btnPlay.hidden=true;
    btnPause.hidden=false;
    //url = [[NSURL alloc] initWithString:[arrURL objectAtIndex:indexPath.row]];
    if([strPlay isEqualToString:@"ViewWillAppear"] || [strPlay isEqualToString:@"Stop"])
    {
        [currentTimeSlider setValue:0.0];
        lblStart.text = @"0:00";
        [self setupAVPlayerForURL:url];
    }
    AVPlayerItem *item = player.currentItem;
    [item addObserver:self forKeyPath:@"timedMetadata" options:NSKeyValueObservingOptionInitial| NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld| NSKeyValueObservingOptionPrior context:nil];
    [player play];
}

- (IBAction)btnPause_Click:(id)sender {
    btnPlay.hidden=false;
    btnPause.hidden=true;
    strPlay = @"Pause";
    [player pause];
}
- (IBAction)btnStop_Click:(id)sender {
    btnPlay.hidden=false;
    btnPause.hidden=true;
    strPlay = @"Stop";
    //[player removeObserver:self forKeyPath:@"status"];
    [player pause];
    player = nil;

}
- (IBAction)btnNext_Click:(id)sender {
    int index = selectedSongIndex;
    if(selectedSongIndex == [arrURL count]-1)
    {
        if(player!= nil)
        {
            [player.currentItem removeObserver:self forKeyPath:@"status"];
        }
        [player pause];
        player = nil;

        AVPlayerItem *item = player.currentItem;
        [item removeObserver:self forKeyPath:@"timedMetadata"];

        selectedSongIndex=0;
        url = [[NSURL alloc] initWithString:[arrURL objectAtIndex:0]];
        [self setupAVPlayerForURL:url];
        AVPlayerItem *item1 = player.currentItem;
        [item1 addObserver:self forKeyPath:@"timedMetadata" options:NSKeyValueObservingOptionInitial| NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld| NSKeyValueObservingOptionPrior context:nil];
        [player play];
    }
    else if(index < [arrURL count])
    {
        if(player!= nil)
        {
            [player.currentItem removeObserver:self forKeyPath:@"status"];
        }
        [player pause];
        player = nil;

        AVPlayerItem *item = player.currentItem;
        [item removeObserver:self forKeyPath:@"timedMetadata"];
        index = selectedSongIndex+1;
        selectedSongIndex = index;
        url = [[NSURL alloc] initWithString:[arrURL objectAtIndex:index]];
        [self setupAVPlayerForURL:url];
        AVPlayerItem *item1 = player.currentItem;
        [item1 addObserver:self forKeyPath:@"timedMetadata" options:NSKeyValueObservingOptionInitial| NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld| NSKeyValueObservingOptionPrior context:nil];
        [player play];
    }
}  
-(void) setupAVPlayerForURL: (NSURL*) url1 {
    AVAsset *asset = [AVURLAsset URLAssetWithURL:url1 options:nil];
    AVPlayerItem *anItem = [AVPlayerItem playerItemWithAsset:asset];
    if(player!= nil)
    {
        [player.currentItem removeObserver:self forKeyPath:@"status"];
    }
    player = [AVPlayer playerWithPlayerItem:anItem];
    [player.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil];
    [player play];
}  
- (CMTime)playerItemDuration
{
    AVPlayerItem *thePlayerItem = [player currentItem];
    if (thePlayerItem.status == AVPlayerItemStatusReadyToPlay)
    {

        return([thePlayerItem duration]);
    }

    return(kCMTimeInvalid);
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if([keyPath isEqualToString:@"timedMetadata"])
    {
        AVPlayerItem *item = (AVPlayerItem *)object;
        NSLog(@"Item.timedMetadata: %@",item.timedMetadata);
        NSLog(@"-- META DATA ---");
        //        AVPlayerItem *pItem = (AVPlayerItem *)object;
        for (AVMetadataItem *metaItem in item.timedMetadata) {
            NSLog(@"meta data = %@",[metaItem commonKey]);
            NSString *key = [metaItem commonKey]; //key = publisher , key = title
            NSString *value = [metaItem stringValue];
            NSLog(@"key = %@, value = %@", key, value);
            if([[metaItem commonKey] isEqualToString:@"title"])
            {
                self.lblTitle.text = [metaItem stringValue];
            }
        }
    }
    if (object == player.currentItem && [keyPath isEqualToString:@"status"]) {
        if (player.status == AVPlayerStatusFailed) {
            NSLog(@"AVPlayer Failed");
        } else if (player.currentItem.status == AVPlayerItemStatusReadyToPlay) {
            NSLog(@"AVPlayer Ready to Play");

            NSTimeInterval totalSeconds = CMTimeGetSeconds(player.currentItem.asset.duration);
            int minutes = (int)totalSeconds / 60;
            int seconds = (int)totalSeconds % 60;
            NSString *minutes1;
            NSString *seconds1;
            if (minutes < 10) {
                minutes1=[NSString stringWithFormat:@"%02d",minutes];
                seconds1=[NSString stringWithFormat:@"%02d",seconds];
            }
            lblEnd.text = [NSString stringWithFormat:@"%@:%@",minutes1,seconds1];
            NSLog(@"lblEnd Duration: %@",lblEnd.text);
            double interval = .1f;

            CMTime playerDuration = [self playerItemDuration]; // return player duration.
            if (CMTIME_IS_INVALID(playerDuration))
            {
                return;
            }
            double duration = CMTimeGetSeconds(playerDuration);
            if (isfinite(duration))
            {
                CGFloat width = CGRectGetWidth([currentTimeSlider bounds]);
                interval = 0.5f * duration / width;
            }

            /* Update the scrubber during normal playback. */
            id timeObserver = [player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC)
                                                                 queue:NULL
                                                            usingBlock:
                             ^(CMTime time)
                             {
                                 [self syncScrubber];
                                 NSLog(@"Available: %f",[self availableDuration]);
                             }];

        } else if (player.status == AVPlayerItemStatusUnknown) {
            NSLog(@"AVPlayer Unknown");
        }
    }
}
- (IBAction)currentTimeSliderValueChanged:(id)sender {
    CMTime playerDuration = [self playerItemDuration];
    double duration = CMTimeGetSeconds(playerDuration);
    float minValue = [currentTimeSlider minimumValue];
    float maxValue = [currentTimeSlider maximumValue];
    double time = CMTimeGetSeconds([player currentTime]);

    int32_t timeScale = self.player.currentItem.asset.duration.timescale;
    [player seekToTime:CMTimeMake((maxValue - minValue) * time / duration + minValue, 1)];
}

- (NSTimeInterval) availableDuration;
{
    int result1 = 0;
    NSArray *loadedTimeRanges = [[self.player currentItem] loadedTimeRanges];
    if([loadedTimeRanges count]>0)
    {
        CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
        Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
        Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval result = startSeconds + durationSeconds;
        result1 =result;
    }
    return result1;
}   
#pragma mark -
#pragma mark Music scrubber control

/* Cancels the previously registered time observer. */
-(void)removePlayerTimeObserver
{
    if (mTimeObserver)
    {
        [self.player removeTimeObserver:mTimeObserver];
        mTimeObserver = nil;
    }
}

/* Requests invocation of a given block during media playback to update the movie scrubber control. */
-(void)initScrubberTimer
{
    double interval = .1f;

    CMTime playerDuration = [self playerItemDuration];
    if (CMTIME_IS_INVALID(playerDuration))
    {
        return;
    }
    double duration = CMTimeGetSeconds(playerDuration);
    if (isfinite(duration))
    {
        CGFloat width = CGRectGetWidth([self.currentTimeSlider bounds]);
        interval = 0.5f * duration / width;
    }

    /* Update the scrubber during normal playback. */
    __weak playerScreenViewController *weakSelf = self;
     mTimeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC)
                                                               queue:NULL /* If you pass NULL, the main queue is used. */
                                                          usingBlock:^(CMTime time)
                     {
                         [weakSelf syncScrubber];
                     }];
}

/* Set the scrubber based on the player current time. */
- (void)syncScrubber
{
    CMTime playerDuration = [self playerItemDuration];
    if (CMTIME_IS_INVALID(playerDuration))
    {
        currentTimeSlider.minimumValue = 0.0;
        return;
    }

    double duration = CMTimeGetSeconds(playerDuration);
    if (isfinite(duration))
    {
        float minValue = [self.currentTimeSlider minimumValue];
        float maxValue = [self.currentTimeSlider maximumValue];
        double time = CMTimeGetSeconds([self.player currentTime]);

        [self.currentTimeSlider setValue:(maxValue - minValue) * time / duration + minValue];

        int minutes = (int)time / 60;
        int seconds = (int)time % 60;
        NSString *minutes1;
        NSString *seconds1;
        if (minutes < 10) {
            minutes1=[NSString stringWithFormat:@"%02d",minutes];
            seconds1=[NSString stringWithFormat:@"%02d",seconds];
        }
        lblStart.text = [NSString stringWithFormat:@"%@:%@",minutes1,seconds1];
        [currentTimeSlider setValue:(maxValue - minValue) * time / duration + minValue];

        int difference = duration-time;

        if (difference == 0) {
            [self removePlayerTimeObserver];
            [self btnNext_Click:nil];
        }
    }
}

/* The user is dragging the movie controller thumb to scrub through the movie. */
- (IBAction)beginScrubbing:(id)sender
{
    mRestoreAfterScrubbingRate = [self.player rate];
    [self.player setRate:0.f];

    /* Remove previous timer. */
    [self removePlayerTimeObserver];
}

/* Set the player current time to match the scrubber position. */
- (IBAction)scrub:(id)sender
{
    if ([sender isKindOfClass:[UISlider class]] && !isSeeking)
    {
        isSeeking = YES;
        UISlider* slider = sender;

        CMTime playerDuration = [self playerItemDuration];
        if (CMTIME_IS_INVALID(playerDuration)) {
            return;
        }

        double duration = CMTimeGetSeconds(playerDuration);
        if (isfinite(duration))
        {
            float minValue = [slider minimumValue];
            float maxValue = [slider maximumValue];
            float value = [slider value];

            double time = duration * (value - minValue) / (maxValue - minValue);

            [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    isSeeking = NO;
                });
            }];
        }
    }
}

/* The user has released the movie thumb control to stop scrubbing through the movie. */
- (IBAction)endScrubbing:(id)sender
{
    if (!mTimeObserver)
    {
        CMTime playerDuration = [self playerItemDuration];
        if (CMTIME_IS_INVALID(playerDuration))
        {
            return;
        }

        double duration = CMTimeGetSeconds(playerDuration);
        if (isfinite(duration))
        {
            CGFloat width = CGRectGetWidth([self.currentTimeSlider bounds]);
            double tolerance = 0.5f * duration / width;

            __weak playerScreenViewController *weakSelf = self;
            mTimeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(tolerance, NSEC_PER_SEC) queue:NULL usingBlock:
                             ^(CMTime time)
                             {
                                 [weakSelf syncScrubber];
                             }];
        }
    }

    if (mRestoreAfterScrubbingRate)
    {
        [self.player setRate:mRestoreAfterScrubbingRate];
        mRestoreAfterScrubbingRate = 0.f;
    }
}

- (BOOL)isScrubbing
{
    return mRestoreAfterScrubbingRate != 0.f;
}

-(void)enableScrubber
{
    self.currentTimeSlider.enabled = YES;
}

-(void)disableScrubber
{
    self.currentTimeSlider.enabled = NO;
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Mihir Oza
  • 2,768
  • 3
  • 35
  • 61
  • 1
    Got My Solution. Here is the link: [AVPlayer was deallocated while key value observers were still registered with it.][1] [1]: http://stackoverflow.com/questions/26256982/avplayer-was-deallocated-while-key-value-observers-were-still-registered-with-it – Mihir Oza Jun 03 '15 at 11:47

3 Answers3

9

I got that error message because of the following code:

- (void)didPlay {
  if (!_boundaryTimeObserver) {
    _boundaryTimeObserver = 
    [_player addBoundaryTimeObserverForTimes:[NSArray<NSValue *> arrayWithObjects:
                                            [NSValue valueWithCMTime:boundaryTime],
                                              nil]
                                       queue:NULL
                                  usingBlock:^() {}];
  }
}
- (void)didStop {
  if (_boundaryTimeObserver) {
    [_player removeTimeObserver:_boundaryTimeObserver];
    // FORGOT TO SET _boundaryTimeObserver to NIL!!!!
  }
}

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

can happen any time the given time observer isn't associated with the AVPlayer. This could be either because it is associated with another AVPlayer or because it was previously removed from the AVPlayer in question.

Heath Borders
  • 30,998
  • 16
  • 147
  • 256
  • 3
    5 Years later and I had this error again, Your answer was helpful – Husam Nov 12 '19 at 22:56
  • Basically if you play to reuse the same AVPlayer and timer, then don't forget to set the timer to nil after removing it self.timer = nil – Brett Jul 17 '20 at 07:26
7

there are so many checks for this error. for me its time observer. check both time observer and if player.rate == 1.0

See the below:

override func viewDidDisappear(_ animated: Bool) {
    if (self.timeObserver != nil) {
        if player.rate == 1.0 { // it is required as you have to check if player is playing
            player.removeTimeObserver(timeObserver)
            player.pause()
        }
    }
}
Ryan
  • 4,799
  • 1
  • 29
  • 56
Chandramani
  • 871
  • 1
  • 12
  • 11
  • This one has a lot of upvotes, so I guess it worked for folks. Does anyone have an idea of why? If the rate is set to 0, the observer isn't removed automatically, so it is hard to see how this would be a good general case solution. – Tad Aug 31 '23 at 21:59
2

When you remove your time observer, don't forget to set it to nil. Otherwise if you try to remove already removed observer it is not associated with any player and therefore it causes crash.

if let periodicTimeObserver {
   audioPlayer.removeTimeObserver(periodicTimeObserver)
   self.periodicTimeObserver = nil
}
Robert Dresler
  • 10,580
  • 2
  • 22
  • 40