Question

With some help from stackoverflow, I managed to implement a timer that checks the currentPlaybackTime variable in a mediaplayer video every 1/10th of a second.

I also have an NSArray of Cue objects that have a time attribute (NSNumber). I want some code that simply checks if the currentPlaybackTime matches one of the time attributes in the cue array and returns the cue object.

My progress so far:

- (void) PollPlayerTimer_tick:(NSObject *)sender {
    if (self.movieController.playbackState == MPMoviePlaybackStatePlaying)
        self.lastRecordedPlaybackTime = self.movieController.currentPlaybackTime;

        if (decidingStatement) {
        // Do something
        }
}

The decidingStatement has to be something like:

[NSNumber numberWithDouble:self.movieController.currentPlaybackTime] doubleValue] is equal to one of the time attributes in cues. It doesn’t seem to work using [NSNumber numberWithDouble:self.movieController.currentPlaybackTime] doubleValue] == 3.9 (for example) because currentPlaybackTime could be 3.99585 which is not exactly 3.9. It works if I check if currentPlaybackTime is within plus or minus 0.1 seconds of 3.9.

The second problem is that I have to search the cues array to get the cue time. I could iterate through the array to get the time attribute. But iterating over an array ten times a second doesn’t seem like the best practise.

There must be a better way!

Update

danh has a great solution below, the code currently looks like:

- (void) PollPlayerTimer_tick:(NSObject *)sender {

    if (self.movieController.playbackState == MPMoviePlaybackStatePlaying)
        self.lastRecordedPlaybackTime = self.movieController.currentPlaybackTime;
    NSInteger *decidingStatement = [self indexOfCueMatching:[[NSNumber numberWithDouble:self.movieController.currentPlaybackTime] doubleValue] in:self.video.cues];
        if (decidingStatement) {

        }

}

- (NSComparisonResult)number:(NSNumber *)nA nearlyEquals:(NSNumber *)nB within:(double)epsilon {

    double dNa = [nA doubleValue];
    double dNb = [nB doubleValue];

    if (fabs(dNa - dNb) < epsilon) return NSOrderedSame;
    if (dNa < dNb) return NSOrderedAscending;
    return NSOrderedDescending;
}

- (NSInteger)indexOfCueMatching:(NSNumber *)playbackTime in:(NSArray *)cues {

    NSInteger index = self.lastCheckedIndex+1;
    if (index >= cues.count) return NSNotFound;

    NSComparisonResult compResult = [self number:[cues[index] time] nearlyEquals:playbackTime within:0.1];
    while (index<cues.count && compResult == NSOrderedAscending) {
        compResult = [self number:[cues[++index] time] nearlyEquals:playbackTime within:0.1];
    }
    self.lastCheckedIndex = index;

    return (compResult == NSOrderedSame)? index : NSNotFound;
}
Was it helpful?

Solution

Problem one is to match NSNumber doubles within some tolerance. That would look something like this:

- (NSComparisonResult)number:(NSNumber *)nA nearlyEquals:(NSNumber *)nB within:(double)epsilon {

    double diff = [nA doubleValue] - [nB doubleValue];

    if (fabs(diff) < epsilon) return NSOrderedSame;
    else return (diff < 0.0)? NSOrderedAscending : NSOrderedDescending;
}

Problem two is a more algorithmic. Finding the cue in the array of numbers given an arbitrary time from the player is definitely a search, but it's a search with some very big hints: a) the cues are probably sorted, b) the playback times are appearing sequentially, increasing monotonically.

Keeping just a little state, like the index of the last place where you checked, you should need to do very little or no iterating through your array.

@property (assign, nonatomic) NSInteger lastCheckedIndex;

Somewhere, before you begin, assign self.lastCheckedIndex = -1. Then, each time your timer fires, get the current playback time, wrap it as [NSNumber numberWithDouble:, and call something like this:

- (NSInteger)indexOfCueMatching:(NSNumber *)playbackTime in:(NSArray *)cues {

    NSInteger index = self.lastCheckedIndex+1;
    if (index >= cues.count) return NSNotFound;

    NSComparisonResult compResult = [self number:cues[index] nearlyEquals:playbackTime within:0.1];
    while (index<cues.count && compResult == NSOrderedAscending) {
        compResult = [self number:cues[++index] nearlyEquals:playbackTime within:0.1];
    }
    self.lastCheckedIndex = index;

    return (compResult == NSOrderedSame)? index : NSNotFound;
}

This should answer either the index in your cue array that matches, or NSNotFound if no matching cue is found. Moreover, this should update the lastCheckedIndex knowing that the next time coming from the player will be larger than the last, and the next time in your cue array will be larger as well.

EDIT - specifically, use it like this...

NSTimeInterval playbackTime = self.movieController.currentPlaybackTime;
self.lastRecordedPlaybackTime = playbackTime;

NSNumber *playbackTimeNumber = [NSNumber numberWithDouble:playbackTime];
NSInteger index = [self indexOfCueMatching:playbackTimeNumber in:self.video.cues];

if (index != NSNotFound) {
    // whatever we do when we find a matching cue
}

Hope that's helpful.

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