Question

After setting mPlayer.usesExternalPlaybackWhileExternalScreenIsActive to YES in AVPlayerDemoPlaybackViewController of Apple's AVPlayerDemo sample project, how do you throttle the scrubbing so it doesn't lag behind on the AppleTV?

What I mean is that when you move the slider really fast back and forth the AppleTV performs each and every seekToTime operation, but takes longer to do it then the user takes to slide.

One of the problems with the demo is it uses both the "Touch Drag Inside" and "Value Changed" events which causes it to send the same value twice. If you remove "Value Changed" it improves a bit, but still lags.

I've tried rounding to whole seconds and then only send seekToTime when the second changes, but that doesn't seem to help as much. What I really need to do is send fewer commands the faster the user moves the slider, but more when the user moves slower.

Any ideas on how to accomplish this?

Was it helpful?

Solution

The UISlider already somewhat throttles itself. The faster you move it the fewer values you get from point A to point B. This isn't enough to stop the seek operations from stacking up over AirPlay.

You can, however, use the seekToTime:completionHandler: to prevent the stack up like this:

if(seeking) {
    return;
}

seeking = YES;
[player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
    seeking = NO;
}];

This drops any new seeks until the one in progress finishes. This seems to work well. You just need to make sure to send one last seek operation after the user stops scrubbing.

While an NSTimer can do the same thing, it's less accurate, and results will vary depending on the latency of the connection. The completionHandler used in this manner ensures that seeks do not stack up, regardless of latency times.

I also found that the "Value Changed" action of the UISlider can happen before any touch start actions. So it's better to use the touch drag inside/outside actions instead, which are guaranteed to happen after a touch start.

OTHER TIPS

Improved Luke answer with some additional code:

static NSTimeInterval ToleranceForAsset(AVAsset *asset) {
    NSTimeInterval tolerance = 0.0;
    for (AVAssetTrack *track in asset.tracks) {
        NSTimeInterval trackTolerance = CMTimeGetSeconds(track.minFrameDuration);
        tolerance = MAX(tolerance, trackTolerance);
    }
    return tolerance;
}

@interface MyPlayerWrapper ()

@property (strong, nonatomic) AVPlayer *player;
@property (assign, nonatomic) NSTimeInterval playerTime;
@property (assign, nonatomic, getter=isSeeking) BOOL seeking;
@property (assign, nonatomic) CGFloat latestSetTime;

@end

@implementation MyPlayerWrapper

- (NSTimeInterval)playerTime {
    return CMTimeGetSeconds(self.player.currentItem.currentTime);
}

- (void)setPlayerTime:(NSTimeInterval)playerTime {
    NSTimeInterval tolerance = ToleranceForAsset(self.player.currentItem.asset);
    if (tolerance) {
        // round to nearest seek tolerance (for example 1/30 sec)
        playerTime = floor(playerTime / tolerance) * tolerance;
    }

    self.latestSetTime = playerTime;
    if (self.isSeeking) {
        return;
    }

    self.seeking = YES;
    [self.player seekToTime:CMTimeMakeWithSeconds(playerTime, self.player.currentItem.duration.timescale) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
        self.seeking = NO;
        if (ABS(self.player.currentItem.currentTime - latestSetTime) > MAX(tolerance, DBL_EPSILON)) {
            self.playerTime = latestSetTime;
        }
    }];
}

@end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top