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:
parent
8952ccac32
commit
d810352f2c
@ -67,6 +67,6 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
|||||||
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
||||||
|
|
||||||
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
||||||
assertThat(seeker.hasPendingSeek()).isTrue();
|
assertThat(seeker.isSeeking()).isTrue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
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.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
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
|
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
|
||||||
* frame that contains the target sample.
|
* 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 FlacDecoderJni decoderJni;
|
||||||
|
|
||||||
private final long firstFramePosition;
|
|
||||||
private final long inputLength;
|
|
||||||
private final long approxBytesPerFrame;
|
|
||||||
|
|
||||||
private @Nullable SeekOperationParams pendingSeekOperationParams;
|
|
||||||
|
|
||||||
public FlacBinarySearchSeeker(
|
public FlacBinarySearchSeeker(
|
||||||
FlacStreamInfo streamInfo,
|
FlacStreamInfo streamInfo,
|
||||||
long firstFramePosition,
|
long firstFramePosition,
|
||||||
long inputLength,
|
long inputLength,
|
||||||
FlacDecoderJni decoderJni) {
|
FlacDecoderJni decoderJni) {
|
||||||
this.streamInfo = Assertions.checkNotNull(streamInfo);
|
super(
|
||||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
new FlacSeekTimestampConverter(streamInfo),
|
||||||
this.firstFramePosition = firstFramePosition;
|
new FlacTimestampSeeker(decoderJni),
|
||||||
this.inputLength = inputLength;
|
|
||||||
this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
|
|
||||||
|
|
||||||
pendingSeekOperationParams = null;
|
|
||||||
seekMap =
|
|
||||||
new FlacBinarySearchSeekMap(
|
|
||||||
streamInfo,
|
|
||||||
firstFramePosition,
|
|
||||||
inputLength,
|
|
||||||
streamInfo.durationUs(),
|
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. */
|
@Override
|
||||||
public SeekMap getSeekMap() {
|
protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
|
||||||
return seekMap;
|
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. */
|
private static final class FlacTimestampSeeker implements TimestampSeeker {
|
||||||
public void setSeekTargetUs(long timeUs) {
|
|
||||||
if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
|
private final FlacDecoderJni decoderJni;
|
||||||
return;
|
|
||||||
}
|
private FlacTimestampSeeker(FlacDecoderJni decoderJni) {
|
||||||
|
this.decoderJni = decoderJni;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
decoderJni.reset(searchPosition);
|
||||||
try {
|
try {
|
||||||
decoderJni.decodeSampleWithBacktrackPosition(
|
decoderJni.decodeSampleWithBacktrackPosition(
|
||||||
@ -145,11 +83,10 @@ import java.nio.ByteBuffer;
|
|||||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||||
// For some reasons, the extractor can't find a frame mid-stream.
|
// 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.
|
// Stop the seeking and let it re-try playing at the last search position.
|
||||||
pendingSeekOperationParams = null;
|
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||||
throw new IOException("Cannot read frame at position " + searchPosition, e);
|
|
||||||
}
|
}
|
||||||
if (outputBuffer.limit() == 0) {
|
if (outputBuffer.limit() == 0) {
|
||||||
return Extractor.RESULT_END_OF_INPUT;
|
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
|
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
|
||||||
@ -157,184 +94,42 @@ import java.nio.ByteBuffer;
|
|||||||
long nextFrameSamplePosition = decoderJni.getDecodePosition();
|
long nextFrameSamplePosition = decoderJni.getDecodePosition();
|
||||||
|
|
||||||
boolean targetSampleInLastFrame =
|
boolean targetSampleInLastFrame =
|
||||||
lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
|
lastFrameSampleIndex <= targetSampleIndex && nextFrameSampleIndex > targetSampleIndex;
|
||||||
&& nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
|
|
||||||
|
|
||||||
if (targetSampleInLastFrame) {
|
if (targetSampleInLastFrame) {
|
||||||
pendingSeekOperationParams = null;
|
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
|
||||||
return Extractor.RESULT_CONTINUE;
|
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
|
||||||
}
|
return TimestampSearchResult.targetFoundResult(input.getPosition());
|
||||||
|
} else if (nextFrameSampleIndex <= targetSampleIndex) {
|
||||||
if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
|
return TimestampSearchResult.underestimatedResult(
|
||||||
pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
|
nextFrameSampleIndex, nextFrameSamplePosition);
|
||||||
} else {
|
} else {
|
||||||
pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
|
return TimestampSearchResult.overestimatedResult(lastFrameSampleIndex, searchPosition);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean skipInputUntilPosition(ExtractorInput input, long position)
|
@Override
|
||||||
throws IOException, InterruptedException {
|
public int getTimestampSearchBytesRange() {
|
||||||
long bytesToSkip = position - input.getPosition();
|
// We rely on decoderJni to search for timestamp (sample index) from a given stream point, so
|
||||||
if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
|
// we don't restrict the range at all.
|
||||||
input.skipFully((int) bytesToSkip);
|
return C.LENGTH_UNSET;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
|
* A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
|
||||||
*
|
* the timestamp for a stream seek time position.
|
||||||
* <p>This class holds parameters for a binary-search for the {@code targetSample} in the range
|
|
||||||
* [floorPosition, ceilingPosition).
|
|
||||||
*/
|
*/
|
||||||
private static final class SeekOperationParams {
|
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
|
||||||
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 final FlacStreamInfo streamInfo;
|
private final FlacStreamInfo streamInfo;
|
||||||
private final long firstFramePosition;
|
|
||||||
private final long inputLength;
|
|
||||||
private final long approxBytesPerFrame;
|
|
||||||
private final long durationUs;
|
|
||||||
|
|
||||||
private FlacBinarySearchSeekMap(
|
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
|
||||||
FlacStreamInfo streamInfo,
|
|
||||||
long firstFramePosition,
|
|
||||||
long inputLength,
|
|
||||||
long durationUs,
|
|
||||||
long approxBytesPerFrame) {
|
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
this.firstFramePosition = firstFramePosition;
|
|
||||||
this.inputLength = inputLength;
|
|
||||||
this.approxBytesPerFrame = approxBytesPerFrame;
|
|
||||||
this.durationUs = durationUs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSeekable() {
|
public long timeUsToTargetTime(long timeUs) {
|
||||||
return true;
|
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import android.support.annotation.IntDef;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
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.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
@ -53,8 +54,7 @@ public final class FlacExtractor implements Extractor {
|
|||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef(
|
@IntDef(
|
||||||
flag = true,
|
flag = true,
|
||||||
value = {FLAG_DISABLE_ID3_METADATA}
|
value = {FLAG_DISABLE_ID3_METADATA})
|
||||||
)
|
|
||||||
public @interface Flags {}
|
public @interface Flags {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,6 +79,7 @@ public final class FlacExtractor implements Extractor {
|
|||||||
|
|
||||||
private ParsableByteArray outputBuffer;
|
private ParsableByteArray outputBuffer;
|
||||||
private ByteBuffer outputByteBuffer;
|
private ByteBuffer outputByteBuffer;
|
||||||
|
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
|
||||||
private FlacStreamInfo streamInfo;
|
private FlacStreamInfo streamInfo;
|
||||||
|
|
||||||
private Metadata id3Metadata;
|
private Metadata id3Metadata;
|
||||||
@ -131,7 +132,7 @@ public final class FlacExtractor implements Extractor {
|
|||||||
decoderJni.setData(input);
|
decoderJni.setData(input);
|
||||||
readPastStreamInfo(input);
|
readPastStreamInfo(input);
|
||||||
|
|
||||||
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
|
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
|
||||||
return handlePendingSeek(input, seekPosition);
|
return handlePendingSeek(input, seekPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,6 +216,7 @@ public final class FlacExtractor implements Extractor {
|
|||||||
outputFormat(streamInfo);
|
outputFormat(streamInfo);
|
||||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||||
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
||||||
|
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
|
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
|
||||||
@ -277,9 +279,10 @@ public final class FlacExtractor implements Extractor {
|
|||||||
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws InterruptedException, IOException {
|
throws InterruptedException, IOException {
|
||||||
int seekResult =
|
int seekResult =
|
||||||
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
|
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
|
||||||
|
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||||
writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
|
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
|
||||||
}
|
}
|
||||||
return seekResult;
|
return seekResult;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user