Refactor FlacBinarySearchSeeker.

Rewrite FlacBinarySearchSeeker and extract out the core binary search algorithm
into BinarySearchSeeker class so it can be re-used for other formats.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=206012900
This commit is contained in:
hoangtc 2018-07-25 10:17:21 -07:00 committed by Oliver Woodman
parent 8952ccac32
commit d810352f2c
4 changed files with 645 additions and 266 deletions

View File

@ -67,6 +67,6 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.hasPendingSeek()).isTrue();
assertThat(seeker.isSeeking()).isTrue();
}
}

View File

@ -15,15 +15,12 @@
*/
package com.google.android.exoplayer2.ext.flac;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -33,111 +30,52 @@ import java.nio.ByteBuffer;
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
* frame that contains the target sample.
*/
/* package */ final class FlacBinarySearchSeeker {
/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {
/**
* When seeking within the source, if the offset is smaller than or equal to this value, the seek
* operation will be performed using a skip operation. Otherwise, the source will be reloaded at
* the new seek position.
*/
private static final long MAX_SKIP_BYTES = 256 * 1024;
private final FlacStreamInfo streamInfo;
private final FlacBinarySearchSeekMap seekMap;
private final FlacDecoderJni decoderJni;
private final long firstFramePosition;
private final long inputLength;
private final long approxBytesPerFrame;
private @Nullable SeekOperationParams pendingSeekOperationParams;
public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
this.streamInfo = Assertions.checkNotNull(streamInfo);
this.decoderJni = Assertions.checkNotNull(decoderJni);
this.firstFramePosition = firstFramePosition;
this.inputLength = inputLength;
this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
pendingSeekOperationParams = null;
seekMap =
new FlacBinarySearchSeekMap(
streamInfo,
firstFramePosition,
inputLength,
super(
new FlacSeekTimestampConverter(streamInfo),
new FlacTimestampSeeker(decoderJni),
streamInfo.durationUs(),
approxBytesPerFrame);
/* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamInfo.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
/** Returns the seek map for the wrapped FLAC stream. */
public SeekMap getSeekMap() {
return seekMap;
@Override
protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
if (!foundTargetFrame) {
// If we can't find the target frame (sample), we need to reset the decoder jni so that
// it can continue from the result position.
decoderJni.reset(resultPosition);
}
}
/** Sets the target time in microseconds within the stream to seek to. */
public void setSeekTargetUs(long timeUs) {
if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
return;
}
pendingSeekOperationParams =
new SeekOperationParams(
timeUs,
streamInfo.getSampleIndex(timeUs),
/* floorSample= */ 0,
/* ceilingSample= */ streamInfo.totalSamples,
/* floorPosition= */ firstFramePosition,
/* ceilingPosition= */ inputLength,
approxBytesPerFrame);
}
/** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
public boolean hasPendingSeek() {
return pendingSeekOperationParams != null;
}
/**
* Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
* {@link Extractor}.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
* to hold the position of the required seek.
* @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe
* updated to hold the extracted frame that contains the target sample. The caller needs to
* check the byte buffer limit to see if an extracted frame is available.
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
public int handlePendingSeek(
ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer)
throws InterruptedException, IOException {
outputBuffer.position(0);
outputBuffer.limit(0);
while (true) {
long floorPosition = pendingSeekOperationParams.floorPosition;
long ceilingPosition = pendingSeekOperationParams.ceilingPosition;
long searchPosition = pendingSeekOperationParams.nextSearchPosition;
// streamInfo may not contain minFrameSize, in which case this value will be 0.
int minFrameSize = Math.max(1, streamInfo.minFrameSize);
if (floorPosition + minFrameSize >= ceilingPosition) {
// The seeking range is too small for more than 1 frame, so we can just continue from
// the floor position.
pendingSeekOperationParams = null;
decoderJni.reset(floorPosition);
return seekToPosition(input, floorPosition, seekPositionHolder);
}
if (!skipInputUntilPosition(input, searchPosition)) {
return seekToPosition(input, searchPosition, seekPositionHolder);
private static final class FlacTimestampSeeker implements TimestampSeeker {
private final FlacDecoderJni decoderJni;
private FlacTimestampSeeker(FlacDecoderJni decoderJni) {
this.decoderJni = decoderJni;
}
@Override
public TimestampSearchResult searchForTimestamp(
ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException {
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
long searchPosition = input.getPosition();
int searchRangeBytes = getTimestampSearchBytesRange();
decoderJni.reset(searchPosition);
try {
decoderJni.decodeSampleWithBacktrackPosition(
@ -145,11 +83,10 @@ import java.nio.ByteBuffer;
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
// For some reasons, the extractor can't find a frame mid-stream.
// Stop the seeking and let it re-try playing at the last search position.
pendingSeekOperationParams = null;
throw new IOException("Cannot read frame at position " + searchPosition, e);
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
}
if (outputBuffer.limit() == 0) {
return Extractor.RESULT_END_OF_INPUT;
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
}
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
@ -157,184 +94,42 @@ import java.nio.ByteBuffer;
long nextFrameSamplePosition = decoderJni.getDecodePosition();
boolean targetSampleInLastFrame =
lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
&& nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
lastFrameSampleIndex <= targetSampleIndex && nextFrameSampleIndex > targetSampleIndex;
if (targetSampleInLastFrame) {
pendingSeekOperationParams = null;
return Extractor.RESULT_CONTINUE;
}
if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
return TimestampSearchResult.targetFoundResult(input.getPosition());
} else if (nextFrameSampleIndex <= targetSampleIndex) {
return TimestampSearchResult.underestimatedResult(
nextFrameSampleIndex, nextFrameSamplePosition);
} else {
pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
}
return TimestampSearchResult.overestimatedResult(lastFrameSampleIndex, searchPosition);
}
}
private boolean skipInputUntilPosition(ExtractorInput input, long position)
throws IOException, InterruptedException {
long bytesToSkip = position - input.getPosition();
if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
input.skipFully((int) bytesToSkip);
return true;
}
return false;
}
private int seekToPosition(
ExtractorInput input, long position, PositionHolder seekPositionHolder) {
if (position == input.getPosition()) {
return Extractor.RESULT_CONTINUE;
} else {
seekPositionHolder.position = position;
return Extractor.RESULT_SEEK;
@Override
public int getTimestampSearchBytesRange() {
// We rely on decoderJni to search for timestamp (sample index) from a given stream point, so
// we don't restrict the range at all.
return C.LENGTH_UNSET;
}
}
/**
* Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
*
* <p>This class holds parameters for a binary-search for the {@code targetSample} in the range
* [floorPosition, ceilingPosition).
* A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
* the timestamp for a stream seek time position.
*/
private static final class SeekOperationParams {
private final long seekTimeUs;
private final long targetSample;
private final long approxBytesPerFrame;
private long floorSample;
private long ceilingSample;
private long floorPosition;
private long ceilingPosition;
private long nextSearchPosition;
private SeekOperationParams(
long seekTimeUs,
long targetSample,
long floorSample,
long ceilingSample,
long floorPosition,
long ceilingPosition,
long approxBytesPerFrame) {
this.seekTimeUs = seekTimeUs;
this.floorSample = floorSample;
this.ceilingSample = ceilingSample;
this.floorPosition = floorPosition;
this.ceilingPosition = ceilingPosition;
this.targetSample = targetSample;
this.approxBytesPerFrame = approxBytesPerFrame;
updateNextSearchPosition();
}
/** Updates the floor constraints (inclusive) of the seek operation. */
private void updateSeekFloor(long floorSample, long floorPosition) {
this.floorSample = floorSample;
this.floorPosition = floorPosition;
updateNextSearchPosition();
}
/** Updates the ceiling constraints (exclusive) of the seek operation. */
private void updateSeekCeiling(long ceilingSample, long ceilingPosition) {
this.ceilingSample = ceilingSample;
this.ceilingPosition = ceilingPosition;
updateNextSearchPosition();
}
private void updateNextSearchPosition() {
this.nextSearchPosition =
getNextSearchPosition(
targetSample,
floorSample,
ceilingSample,
floorPosition,
ceilingPosition,
approxBytesPerFrame);
}
/**
* Returns the next position in FLAC stream to search for target sample, given [floorPosition,
* ceilingPosition).
*/
private static long getNextSearchPosition(
long targetSample,
long floorSample,
long ceilingSample,
long floorPosition,
long ceilingPosition,
long approxBytesPerFrame) {
if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) {
return floorPosition;
}
long samplesToSkip = targetSample - floorSample;
long estimatedBytesPerSample =
Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample));
// In the stream, the samples are accessed in a group of frame. Given a stream position, the
// seeker will be able to find the first frame following that position.
// Hence, if our target sample is in the middle of a frame, and our estimate position is
// correct, or very near the actual sample position, the seeker will keep accessing the next
// frame, rather than the frame that contains the target sample.
// Moreover, it's better to under-estimate rather than over-estimate, because the extractor
// input can skip forward easily, but cannot rewind easily (it may require a new connection
// to be made).
// Therefore, we should reduce the estimated position by some amount, so it will converge to
// the correct frame earlier.
long bytesToSkip = samplesToSkip * estimatedBytesPerSample;
long confidenceInterval = bytesToSkip / 20;
long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1);
long estimatedPosition = estimatedFramePosition - confidenceInterval;
return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1);
}
}
/**
* A {@link SeekMap} implementation that returns the estimated byte location from {@link
* SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link
* #getSeekPoints(long)} query.
*/
private static final class FlacBinarySearchSeekMap implements SeekMap {
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamInfo streamInfo;
private final long firstFramePosition;
private final long inputLength;
private final long approxBytesPerFrame;
private final long durationUs;
private FlacBinarySearchSeekMap(
FlacStreamInfo streamInfo,
long firstFramePosition,
long inputLength,
long durationUs,
long approxBytesPerFrame) {
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
this.streamInfo = streamInfo;
this.firstFramePosition = firstFramePosition;
this.inputLength = inputLength;
this.approxBytesPerFrame = approxBytesPerFrame;
this.durationUs = durationUs;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
long nextSearchPosition =
SeekOperationParams.getNextSearchPosition(
streamInfo.getSampleIndex(timeUs),
/* floorSample= */ 0,
/* ceilingSample= */ streamInfo.totalSamples,
/* floorPosition= */ firstFramePosition,
/* ceilingPosition= */ inputLength,
approxBytesPerFrame);
return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
}
@Override
public long getDurationUs() {
return durationUs;
public long timeUsToTargetTime(long timeUs) {
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
}
}
}

View File

@ -21,6 +21,7 @@ import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -53,8 +54,7 @@ public final class FlacExtractor implements Extractor {
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_DISABLE_ID3_METADATA}
)
value = {FLAG_DISABLE_ID3_METADATA})
public @interface Flags {}
/**
@ -79,6 +79,7 @@ public final class FlacExtractor implements Extractor {
private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer;
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata;
@ -131,7 +132,7 @@ public final class FlacExtractor implements Extractor {
decoderJni.setData(input);
readPastStreamInfo(input);
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition);
}
@ -215,6 +216,7 @@ public final class FlacExtractor implements Extractor {
outputFormat(streamInfo);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
}
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
@ -277,9 +279,10 @@ public final class FlacExtractor implements Extractor {
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
throws InterruptedException, IOException {
int seekResult =
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
}
return seekResult;
}

View File

@ -0,0 +1,581 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
/**
* A seeker that supports seeking within a stream by searching for the target frame using binary
* search.
*
* <p>This seeker operates on a stream that contains multiple frames (or samples). Each frame is
* associated with some kind of timestamps, such as stream time, or frame indices. Given a target
* seek time, the seeker will find the corresponding target timestamp, and perform a search
* operation within the stream to identify the target frame and return the byte position in the
* stream of the target frame.
*/
public abstract class BinarySearchSeeker {
/** A seeker that looks for a given timestamp from an input. */
protected interface TimestampSeeker {
/**
* Searches for a given timestamp from the input.
*
* <p>Given a target timestamp and an input stream, this seeker will try to read up to a range
* of {@code searchRangeBytes} bytes from that input, look for all available timestamps from all
* frames in that range, compare those with the target timestamp, and return one of the {@link
* TimestampSearchResult}.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param targetTimestamp The target timestamp that we are looking for.
* @param outputFrameHolder If {@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is
* returned, this holder may be updated to hold the extracted frame that contains the target
* frame/sample associated with the target timestamp.
* @return A {@link TimestampSearchResult}, that includes a {@link TimestampSearchResult#result}
* value, and other necessary info:
* <ul>
* <li>{@link TimestampSearchResult#RESULT_NO_TIMESTAMP} is returned if there is no
* timestamp in the reading range.
* <li>{@link TimestampSearchResult#RESULT_POSITION_UNDERESTIMATED} is returned if all
* timestamps in the range are smaller than the target timestamp.
* <li>{@link TimestampSearchResult#RESULT_POSITION_OVERESTIMATED} is returned if all
* timestamps in the range are larger than the target timestamp.
* <li>{@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is returned if this
* seeker can find a timestamp that it deems close enough to the given target.
* </ul>
*
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
TimestampSearchResult searchForTimestamp(
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException;
/**
* The range of bytes from the current input position from which to search for the target
* timestamp. Uses {@link C#LENGTH_UNSET} to signal that there is no limit for the search range.
*
* @see #searchForTimestamp(ExtractorInput, long, OutputFrameHolder)
*/
int getTimestampSearchBytesRange();
}
/**
* Holds a frame extracted from a stream, together with the time stamp of the frame in
* microseconds.
*/
public static final class OutputFrameHolder {
public long timeUs;
public ByteBuffer byteBuffer;
/** Constructs an instance, wrapping the given byte buffer. */
public OutputFrameHolder(ByteBuffer outputByteBuffer) {
this.timeUs = 0;
this.byteBuffer = outputByteBuffer;
}
}
/**
* A converter that converts seek time in stream time into target timestamp for the {@link
* BinarySearchSeeker}.
*/
protected interface SeekTimestampConverter {
/**
* Converts a seek time in microseconds into target timestamp for the {@link
* BinarySearchSeeker}.
*/
long timeUsToTargetTime(long timeUs);
}
/**
* When seeking within the source, if the offset is smaller than or equal to this value, the seek
* operation will be performed using a skip operation. Otherwise, the source will be reloaded at
* the new seek position.
*/
private static final long MAX_SKIP_BYTES = 256 * 1024;
protected final BinarySearchSeekMap seekMap;
protected final TimestampSeeker timestampSeeker;
protected @Nullable SeekOperationParams seekOperationParams;
private final int minimumSearchRange;
/**
* Constructs an instance.
*
* @param seekTimestampConverter The {@link SeekTimestampConverter} that converts seek time in
* stream time into target timestamp.
* @param timestampSeeker A {@link TimestampSeeker} that will be used to search for timestamps
* within the stream.
* @param durationUs The duration of the stream in microseconds.
* @param floorTimePosition The minimum timestamp value (inclusive) in the stream.
* @param ceilingTimePosition The minimum timestamp value (exclusive) in the stream.
* @param floorBytePosition The starting position of the frame with minimum timestamp value
* (inclusive) in the stream.
* @param ceilingBytePosition The position after the frame with maximum timestamp value in the
* stream.
* @param approxBytesPerFrame Approximated bytes per frame.
* @param minimumSearchRange The minimum byte range that this binary seeker will operate on. If
* the remaining search range is smaller than this value, the search will stop, and the seeker
* will return the position at the floor of the range as the result.
*/
@SuppressWarnings("initialization")
protected BinarySearchSeeker(
SeekTimestampConverter seekTimestampConverter,
TimestampSeeker timestampSeeker,
long durationUs,
long floorTimePosition,
long ceilingTimePosition,
long floorBytePosition,
long ceilingBytePosition,
long approxBytesPerFrame,
int minimumSearchRange) {
this.timestampSeeker = timestampSeeker;
this.minimumSearchRange = minimumSearchRange;
this.seekMap =
new BinarySearchSeekMap(
seekTimestampConverter,
durationUs,
floorTimePosition,
ceilingTimePosition,
floorBytePosition,
ceilingBytePosition,
approxBytesPerFrame);
}
/** Returns the seek map for the stream. */
public final SeekMap getSeekMap() {
return seekMap;
}
/**
* Sets the target time in microseconds within the stream to seek to.
*
* @param timeUs The target time in microseconds within the stream.
*/
public final void setSeekTargetUs(long timeUs) {
if (seekOperationParams != null && seekOperationParams.getSeekTimeUs() == timeUs) {
return;
}
seekOperationParams = createSeekParamsForTargetTimeUs(timeUs);
}
/** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
public final boolean isSeeking() {
return seekOperationParams != null;
}
/**
* Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
* {@link Extractor}.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
* to hold the position of the required seek.
* @param outputFrameHolder If {@link Extractor#RESULT_CONTINUE} is returned, this holder may be
* updated to hold the extracted frame that contains the target sample. The caller needs to
* check the byte buffer limit to see if an extracted frame is available.
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
public int handlePendingSeek(
ExtractorInput input, PositionHolder seekPositionHolder, OutputFrameHolder outputFrameHolder)
throws InterruptedException, IOException {
TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker);
while (true) {
SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams);
long floorPosition = seekOperationParams.getFloorBytePosition();
long ceilingPosition = seekOperationParams.getCeilingBytePosition();
long searchPosition = seekOperationParams.getNextSearchBytePosition();
if (ceilingPosition - floorPosition <= minimumSearchRange) {
// The seeking range is too small, so we can just continue from the floor position.
markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition);
return seekToPosition(input, floorPosition, seekPositionHolder);
}
if (!skipInputUntilPosition(input, searchPosition)) {
return seekToPosition(input, searchPosition, seekPositionHolder);
}
input.resetPeekPosition();
TimestampSearchResult timestampSearchResult =
timestampSeeker.searchForTimestamp(
input, seekOperationParams.getTargetTimePosition(), outputFrameHolder);
switch (timestampSearchResult.result) {
case TimestampSearchResult.RESULT_POSITION_OVERESTIMATED:
seekOperationParams.updateSeekCeiling(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.RESULT_POSITION_UNDERESTIMATED:
seekOperationParams.updateSeekFloor(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.RESULT_TARGET_TIMESTAMP_FOUND:
markSeekOperationFinished(
/* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);
skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);
return seekToPosition(
input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder);
case TimestampSearchResult.RESULT_NO_TIMESTAMP:
// We can't find any timestamp in the search range from the search position.
// Give up, and just continue reading from the last search position in this case.
markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition);
return seekToPosition(input, searchPosition, seekPositionHolder);
default:
throw new IllegalStateException("Invalid case");
}
}
}
protected SeekOperationParams createSeekParamsForTargetTimeUs(long timeUs) {
return new SeekOperationParams(
timeUs,
seekMap.timeUsToTargetTime(timeUs),
seekMap.floorTimePosition,
seekMap.ceilingTimePosition,
seekMap.floorBytePosition,
seekMap.ceilingBytePosition,
seekMap.approxBytesPerFrame);
}
protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
seekOperationParams = null;
onSeekOperationFinished(foundTargetFrame, resultPosition);
}
protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
// Do nothing.
}
protected final boolean skipInputUntilPosition(ExtractorInput input, long position)
throws IOException, InterruptedException {
long bytesToSkip = position - input.getPosition();
if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
input.skipFully((int) bytesToSkip);
return true;
}
return false;
}
protected final int seekToPosition(
ExtractorInput input, long position, PositionHolder seekPositionHolder) {
if (position == input.getPosition()) {
return Extractor.RESULT_CONTINUE;
} else {
seekPositionHolder.position = position;
return Extractor.RESULT_SEEK;
}
}
/**
* Contains parameters for a pending seek operation by {@link BinarySearchSeeker}.
*
* <p>This class holds parameters for a binary-search for the {@code targetTimePosition} in the
* range [floorPosition, ceilingPosition).
*/
protected static class SeekOperationParams {
private final long seekTimeUs;
private final long targetTimePosition;
private final long approxBytesPerFrame;
private long floorTimePosition;
private long ceilingTimePosition;
private long floorBytePosition;
private long ceilingBytePosition;
private long nextSearchBytePosition;
/**
* Returns the next position in the stream to search for target frame, given [floorBytePosition,
* ceilingBytePosition), with corresponding [floorTimePosition, ceilingTimePosition).
*/
protected static long calculateNextSearchBytePosition(
long targetTimePosition,
long floorTimePosition,
long ceilingTimePosition,
long floorBytePosition,
long ceilingBytePosition,
long approxBytesPerFrame) {
if (floorBytePosition + 1 >= ceilingBytePosition
|| floorTimePosition + 1 >= ceilingTimePosition) {
return floorBytePosition;
}
long seekTimeDuration = targetTimePosition - floorTimePosition;
float estimatedBytesPerTimeUnit =
(float) (ceilingBytePosition - floorBytePosition)
/ (ceilingTimePosition - floorTimePosition);
// It's better to under-estimate rather than over-estimate, because the extractor
// input can skip forward easily, but cannot rewind easily (it may require a new connection
// to be made).
// Therefore, we should reduce the estimated position by some amount, so it will converge to
// the correct frame earlier.
long bytesToSkip = (long) (seekTimeDuration * estimatedBytesPerTimeUnit);
long confidenceInterval = bytesToSkip / 20;
long estimatedFramePosition = floorBytePosition + bytesToSkip - approxBytesPerFrame;
long estimatedPosition = estimatedFramePosition - confidenceInterval;
return Util.constrainValue(estimatedPosition, floorBytePosition, ceilingBytePosition - 1);
}
protected SeekOperationParams(
long seekTimeUs,
long targetTimePosition,
long floorTimePosition,
long ceilingTimePosition,
long floorBytePosition,
long ceilingBytePosition,
long approxBytesPerFrame) {
this.seekTimeUs = seekTimeUs;
this.targetTimePosition = targetTimePosition;
this.floorTimePosition = floorTimePosition;
this.ceilingTimePosition = ceilingTimePosition;
this.floorBytePosition = floorBytePosition;
this.ceilingBytePosition = ceilingBytePosition;
this.approxBytesPerFrame = approxBytesPerFrame;
this.nextSearchBytePosition =
calculateNextSearchBytePosition(
targetTimePosition,
floorTimePosition,
ceilingTimePosition,
floorBytePosition,
ceilingBytePosition,
approxBytesPerFrame);
}
/**
* Returns the floor byte position of the range [floorPosition, ceilingPosition) for this seek
* operation.
*/
private long getFloorBytePosition() {
return floorBytePosition;
}
/**
* Returns the ceiling byte position of the range [floorPosition, ceilingPosition) for this seek
* operation.
*/
private long getCeilingBytePosition() {
return ceilingBytePosition;
}
/** Returns the target timestamp as translated from the seek time. */
private long getTargetTimePosition() {
return targetTimePosition;
}
/** Returns the target seek time in microseconds. */
private long getSeekTimeUs() {
return seekTimeUs;
}
/** Updates the floor constraints (inclusive) of the seek operation. */
private void updateSeekFloor(long floorTimePosition, long floorBytePosition) {
this.floorTimePosition = floorTimePosition;
this.floorBytePosition = floorBytePosition;
updateNextSearchBytePosition();
}
/** Updates the ceiling constraints (exclusive) of the seek operation. */
private void updateSeekCeiling(long ceilingTimePosition, long ceilingBytePosition) {
this.ceilingTimePosition = ceilingTimePosition;
this.ceilingBytePosition = ceilingBytePosition;
updateNextSearchBytePosition();
}
/** Returns the next position in the stream to search. */
private long getNextSearchBytePosition() {
return nextSearchBytePosition;
}
private void updateNextSearchBytePosition() {
this.nextSearchBytePosition =
calculateNextSearchBytePosition(
targetTimePosition,
floorTimePosition,
ceilingTimePosition,
floorBytePosition,
ceilingBytePosition,
approxBytesPerFrame);
}
}
/**
* Represents possible search results for {@link
* TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}.
*/
public static final class TimestampSearchResult {
public static final int RESULT_TARGET_TIMESTAMP_FOUND = 0;
public static final int RESULT_POSITION_OVERESTIMATED = -1;
public static final int RESULT_POSITION_UNDERESTIMATED = -2;
public static final int RESULT_NO_TIMESTAMP = -3;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
RESULT_TARGET_TIMESTAMP_FOUND,
RESULT_POSITION_OVERESTIMATED,
RESULT_POSITION_UNDERESTIMATED,
RESULT_NO_TIMESTAMP
})
@interface SearchResult {}
public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT =
new TimestampSearchResult(RESULT_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET);
/** @see TimestampSeeker */
private final @SearchResult int result;
/**
* When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@code
* result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorTimePosition} should be updated with this value.
*/
private final long timestampToUpdate;
/**
* When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@code
* result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorBytePosition} should be updated with this value.
*/
private final long bytePositionToUpdate;
private TimestampSearchResult(
@SearchResult int result, long timestampToUpdate, long bytePositionToUpdate) {
this.result = result;
this.timestampToUpdate = timestampToUpdate;
this.bytePositionToUpdate = bytePositionToUpdate;
}
/**
* Returns a result to signal that the current position in the input stream overestimates the
* true position of the target frame, and the {@link BinarySearchSeeker} should modify its
* {@link SeekOperationParams}'s ceiling timestamp and byte position using the given values.
*/
public static TimestampSearchResult overestimatedResult(
long newCeilingTimestamp, long newCeilingBytePosition) {
return new TimestampSearchResult(
RESULT_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition);
}
/**
* Returns a result to signal that the current position in the input stream underestimates the
* true position of the target frame, and the {@link BinarySearchSeeker} should modify its
* {@link SeekOperationParams}'s floor timestamp and byte position using the given values.
*/
public static TimestampSearchResult underestimatedResult(
long newFloorTimestamp, long newCeilingBytePosition) {
return new TimestampSearchResult(
RESULT_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition);
}
/**
* Returns a result to signal that the target timestamp has been found at the {@code
* resultBytePosition}, and the seek operation can stop.
*
* <p>Note that when this value is returned from {@link
* TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}, the {@link
* OutputFrameHolder} may be updated to hold the target frame as an optimization.
*/
public static TimestampSearchResult targetFoundResult(long resultBytePosition) {
return new TimestampSearchResult(
RESULT_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition);
}
}
/**
* A {@link SeekMap} implementation that returns the estimated byte location from {@link
* SeekOperationParams#calculateNextSearchBytePosition(long, long, long, long, long, long)} for
* each {@link #getSeekPoints(long)} query.
*/
public static class BinarySearchSeekMap implements SeekMap {
private final SeekTimestampConverter seekTimestampConverter;
private final long durationUs;
private final long floorTimePosition;
private final long ceilingTimePosition;
private final long floorBytePosition;
private final long ceilingBytePosition;
private final long approxBytesPerFrame;
/** Constructs a new instance of this seek map. */
public BinarySearchSeekMap(
SeekTimestampConverter seekTimestampConverter,
long durationUs,
long floorTimePosition,
long ceilingTimePosition,
long floorBytePosition,
long ceilingBytePosition,
long approxBytesPerFrame) {
this.seekTimestampConverter = seekTimestampConverter;
this.durationUs = durationUs;
this.floorTimePosition = floorTimePosition;
this.ceilingTimePosition = ceilingTimePosition;
this.floorBytePosition = floorBytePosition;
this.ceilingBytePosition = ceilingBytePosition;
this.approxBytesPerFrame = approxBytesPerFrame;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
long nextSearchPosition =
SeekOperationParams.calculateNextSearchBytePosition(
/* targetTimePosition= */ seekTimestampConverter.timeUsToTargetTime(timeUs),
/* floorTimePosition= */ floorTimePosition,
/* ceilingTimePosition= */ ceilingTimePosition,
/* floorBytePosition= */ floorBytePosition,
/* ceilingBytePosition= */ ceilingBytePosition,
/* approxBytesPerFrame= */ approxBytesPerFrame);
return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
}
@Override
public long getDurationUs() {
return durationUs;
}
/** @see SeekTimestampConverter#timeUsToTargetTime(long) */
public long timeUsToTargetTime(long timeUs) {
return seekTimestampConverter.timeUsToTargetTime(timeUs);
}
}
/**
* A {@link SeekTimestampConverter} implementation that returns the seek time itself as the
* timestamp for a seek time position.
*/
private static final class DefaultSeekTimestampConverter implements SeekTimestampConverter {
@Override
public long timeUsToTargetTime(long timeUs) {
return timeUs;
}
}
}