stephan
stephan

Reputation: 51

seekToTime messes up simultaneous playback of two AVPlayers

Following problem. I have two AVPlayers, each initialized with a different AVPlayerItem. Once the AVPlayerItem is successfully loaded, I add the AVPlayerLayer to the view layer. As you can see in the code below.

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.playerItem = [[AVPlayerItem alloc] initWithURL:[self.video getVideoPath]];
    [self.playerItem addObserver:self forKeyPath:@"status" options:0 context:nil];
    [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:0 context:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(videoEnded)
                                             name:AVPlayerItemDidPlayToEndTimeNotification
                                           object:nil];


    self.player = [[AVPlayer alloc] initWithPlayerItem:playerItem];

    self.overlayPlayerItem = [[AVPlayerItem alloc] initWithURL:[self.compareVideo getVideoPath]];

    [self.overlayPlayerItem addObserver:self forKeyPath:@"status" options:0 context:nil];
    [self.overlayPlayerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:0 context:nil];

    if (self.overlayPlayer==nil) {
        self.overlayPlayer = [[AVPlayer alloc] initWithPlayerItem:self.overlayPlayerItem];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([object isKindOfClass:[AVPlayerItem class]])
    {
        AVPlayerItem *item = (AVPlayerItem *)object;
        //playerItem status value changed?
    if ([keyPath isEqualToString:@"status"])
    {   //yes->check it...
        switch(item.status)
        {
            case AVPlayerItemStatusFailed:
                NSLog(@"player item status failed");
                break;
            case AVPlayerItemStatusReadyToPlay:
            {
                NSLog(@"player item status is ready to play");
                if (item == self.playerItem && !videoLayerAdded) {
                    videoLayerAdded = YES;
                    AVPlayerLayer* layer = [AVPlayerLayer playerLayerWithPlayer:player];
                    layer.frame = self.videoContainer.bounds;
                    [self.videoContainer.layer insertSublayer:layer atIndex:0];
                } else if (item == self.overlayPlayerItem && !overlayLayerAdded){
                    overlayLayerAdded = YES;
                    AVPlayerLayer* layer = [AVPlayerLayer playerLayerWithPlayer:overlayPlayer];
                    layer.frame = self.videoOverlayContainer.bounds;
                    [self.videoOverlayContainer.layer insertSublayer:layer atIndex:0];
                }
                break;
            }
            case AVPlayerItemStatusUnknown:
                NSLog(@"player item status is unknown");
                break;
        }
    }
    else if ([keyPath isEqualToString:@"playbackBufferEmpty"])
    {
        if (item.playbackBufferEmpty)
        {
            NSLog(@"player item playback buffer is empty");
        }
    }
}
}

When I hit the play button the videos get played. If I hit it again they stop, so far so good.

- (IBAction)playButtonClicked:(id)sender{

    //is playing
    if(self.player.rate>0.0 || self.overlayPlayer.rate>0.0){
        [self.player pause];
        [self.overlayPlayer pause];
    } else {
        [self.player play];
        [self.overlayPlayer play];
    }
}

I have two UISlider that allow me to go forth and back through each video. I use seekToTime to jump to a certain time in a video.

- (IBAction)sliderChanged:(id)sender{
    UISlider *slider = (UISlider *)sender;

    //stop any video when using the slider
    if(self.player.rate>0.0){
            [self.player pause];
    }
    if(self.overlayPlayer.rate>0.0){
        [self.overlayPlayer pause];
    }

    if (slider.tag == 1) {
        double time = (self.playerTimeSlider.value * CMTimeGetSeconds(self.player.currentItem.asset.duration));
        [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
    } else if (slider.tag == 2){
        double time = (self.overlayTimeSlider.value * CMTimeGetSeconds(self.overlayPlayer.currentItem.asset.duration));
        [self.overlayPlayer seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
    }
}

Videos can have different length as well. However, if a video ends, I set it back to the start, also with seekToTime.

My problem is that I can initially play both videos. I can pause and resume them without problem. But once I pause and use seekToTime for any video and resume or if the videos are at the end I reset them with seekToTime and hit play again my status observer fires AVPlayerItemStatusFailed for both PlayerItems.

Then the duration of those items becomes 0. I checked though, the PlayerItems are not nil.

I don't get a crash but I just can't play the videos anymore. When I only use one player I can jump through it with seekToTime and also reset it at the end of the video without problem.

I read through the forum and people say you can apparently use up to 4 AVPlayers, so I guess I should be save with 2. I also read about that other apps can use up video render pipelines, but I made sure that I don't have any other apps in the background on my iPad.

Any help as to why this is not working for me or even better, a fix, is highly appreciated.

Update: I actually logged the error from the AVPlayerItem object now:

error: Error Domain=AVFoundationErrorDomain Code=-11819 "Cannot Complete Action"  
UserInfo=0x19e370 {NSLocalizedRecoverySuggestion=Try again later., 
NSLocalizedDescription=Cannot Complete Action}

The code -11819 stands for AVErrorMediaServicesWereReset. Does that help anyone to help me?

Upvotes: 1

Views: 2484

Answers (1)

janela
janela

Reputation: 31

This might be a long shot but take a look at the following page of the documentation:

AV Foundation Programming Guide - Seeking—Repositioning the Playhead

My hint is that by giving a tolerance of zero (before and after) you are probably causing the decoders to work their way out of a key frame calculating the exact requested frame.

This, in conjunction with playing two items at the same time, can cause the exhaustion of resources allocated to the iOS internal Media Services. Thus causing a reset. I guess it depends on numerous factors being the most important the quality and format of the videos being played.

If you do not require precise time control in the scrubbing (!) try to play with the tolerance. I suggest that you try with values slightly larger than the maximum keyframe interval.

That value (the maximum keyframe interval) is, most likely, movie dependent.

If you have control of the movies and can live with insane movie file sizes try to export them using a max keyframe interval of 1 (!) and give it a spin.

Upvotes: 2

Related Questions