blob: b5eb092dfb687de7fcb8eb075a6ce2262d64be24 [file] [log] [blame]
/*
* 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 androidx.annotation.IntDef;
import androidx.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.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 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 a limited window of the provided input for a target timestamp. The size of the
* window is implementation specific, but should be small enough such that it's reasonable for
* multiple such reads to occur during a seek operation.
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @param targetTimestamp The target timestamp.
* @return A {@link TimestampSearchResult} that describes the result of the search.
* @throws IOException If an error occurred reading from the input.
*/
TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
throws IOException;
/** Called when a seek operation finishes. */
default void onSeekFinished() {}
}
/**
* A {@link SeekTimestampConverter} implementation that returns the seek time itself as the
* timestamp for a seek time position.
*/
public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter {
@Override
public long timeUsToTargetTime(long timeUs) {
return timeUs;
}
}
/**
* 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;
@Nullable protected 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.
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
* @throws IOException If an error occurred reading from the input.
*/
public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException {
while (true) {
SeekOperationParams seekOperationParams =
Assertions.checkStateNotNull(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());
switch (timestampSearchResult.type) {
case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:
seekOperationParams.updateSeekCeiling(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED:
seekOperationParams.updateSeekFloor(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND:
skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);
markSeekOperationFinished(
/* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);
return seekToPosition(
input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder);
case TimestampSearchResult.TYPE_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;
timestampSeeker.onSeekFinished();
onSeekOperationFinished(foundTargetFrame, resultPosition);
}
protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
// Do nothing.
}
protected final boolean skipInputUntilPosition(ExtractorInput input, long position)
throws IOException {
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)}.
*/
public static final class TimestampSearchResult {
/** The search found a timestamp that it deems close enough to the given target. */
public static final int TYPE_TARGET_TIMESTAMP_FOUND = 0;
/** The search found only timestamps larger than the target timestamp. */
public static final int TYPE_POSITION_OVERESTIMATED = -1;
/** The search found only timestamps smaller than the target timestamp. */
public static final int TYPE_POSITION_UNDERESTIMATED = -2;
/** The search didn't find any timestamps. */
public static final int TYPE_NO_TIMESTAMP = -3;
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TYPE_TARGET_TIMESTAMP_FOUND,
TYPE_POSITION_OVERESTIMATED,
TYPE_POSITION_UNDERESTIMATED,
TYPE_NO_TIMESTAMP
})
@interface Type {}
public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT =
new TimestampSearchResult(TYPE_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET);
/** The type of the result. */
@Type private final int type;
/**
* When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@link
* #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorTimePosition} should be updated with this value.
*/
private final long timestampToUpdate;
/**
* When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@link
* #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorBytePosition} should be updated with this value.
*/
private final long bytePositionToUpdate;
private TimestampSearchResult(
@Type int type, long timestampToUpdate, long bytePositionToUpdate) {
this.type = type;
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(
TYPE_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(
TYPE_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition);
}
/**
* Returns a result to signal that the target timestamp has been found at {@code
* resultBytePosition}, and the seek operation can stop.
*/
public static TimestampSearchResult targetFoundResult(long resultBytePosition) {
return new TimestampSearchResult(
TYPE_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);
}
}
}