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);
|
||||
|
||||
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
||||
assertThat(seeker.hasPendingSeek()).isTrue();
|
||||
assertThat(seeker.isSeeking()).isTrue();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
super(
|
||||
new FlacSeekTimestampConverter(streamInfo),
|
||||
new FlacTimestampSeeker(decoderJni),
|
||||
streamInfo.durationUs(),
|
||||
/* floorTimePosition= */ 0,
|
||||
/* ceilingTimePosition= */ streamInfo.totalSamples,
|
||||
/* floorBytePosition= */ firstFramePosition,
|
||||
/* ceilingBytePosition= */ inputLength,
|
||||
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
|
||||
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
|
||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||
this.firstFramePosition = firstFramePosition;
|
||||
this.inputLength = inputLength;
|
||||
this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
|
||||
|
||||
pendingSeekOperationParams = null;
|
||||
seekMap =
|
||||
new FlacBinarySearchSeekMap(
|
||||
streamInfo,
|
||||
firstFramePosition,
|
||||
inputLength,
|
||||
streamInfo.durationUs(),
|
||||
approxBytesPerFrame);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
private static final class FlacTimestampSeeker implements TimestampSeeker {
|
||||
|
||||
private final FlacDecoderJni decoderJni;
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -52,9 +53,8 @@ public final class FlacExtractor implements Extractor {
|
||||
/** Flags controlling the behavior of the extractor. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(
|
||||
flag = true,
|
||||
value = {FLAG_DISABLE_ID3_METADATA}
|
||||
)
|
||||
flag = true,
|
||||
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;
|
||||
}
|
||||
|
@ -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