Add flag for constant bitrate seeking even if input length is unknown

PiperOrigin-RevId: 396363113
This commit is contained in:
olly 2021-09-13 16:38:05 +01:00 committed by Christos Tsilopoulos
parent cf0ec91934
commit f8d60e2bbb
6 changed files with 184 additions and 34 deletions

View File

@ -16,9 +16,9 @@
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
/** /**
* A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of
@ -33,9 +33,10 @@ public class ConstantBitrateSeekMap implements SeekMap {
private final long dataSize; private final long dataSize;
private final int bitrate; private final int bitrate;
private final long durationUs; private final long durationUs;
private final boolean allowSeeksIfLengthUnknown;
/** /**
* Constructs a new instance from a stream. * Creates an instance with {@code allowSeeksIfLengthUnknown} set to {@code false}.
* *
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFrameBytePosition The byte-position of the first frame in the stream. * @param firstFrameBytePosition The byte-position of the first frame in the stream.
@ -45,10 +46,36 @@ public class ConstantBitrateSeekMap implements SeekMap {
*/ */
public ConstantBitrateSeekMap( public ConstantBitrateSeekMap(
long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) { long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) {
this(
inputLength,
firstFrameBytePosition,
bitrate,
frameSize,
/* allowSeeksIfLengthUnknown= */ false);
}
/**
* Creates an instance.
*
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFrameBytePosition The byte-position of the first frame in the stream.
* @param bitrate The bitrate (which is assumed to be constant in the stream).
* @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET}
* if unknown.
* @param allowSeeksIfLengthUnknown Whether to allow seeking even if the length of the content is
* unknown.
*/
public ConstantBitrateSeekMap(
long inputLength,
long firstFrameBytePosition,
int bitrate,
int frameSize,
boolean allowSeeksIfLengthUnknown) {
this.inputLength = inputLength; this.inputLength = inputLength;
this.firstFrameBytePosition = firstFrameBytePosition; this.firstFrameBytePosition = firstFrameBytePosition;
this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize; this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize;
this.bitrate = bitrate; this.bitrate = bitrate;
this.allowSeeksIfLengthUnknown = allowSeeksIfLengthUnknown;
if (inputLength == C.LENGTH_UNSET) { if (inputLength == C.LENGTH_UNSET) {
dataSize = C.LENGTH_UNSET; dataSize = C.LENGTH_UNSET;
@ -61,18 +88,23 @@ public class ConstantBitrateSeekMap implements SeekMap {
@Override @Override
public boolean isSeekable() { public boolean isSeekable() {
return dataSize != C.LENGTH_UNSET; return dataSize != C.LENGTH_UNSET || allowSeeksIfLengthUnknown;
} }
@Override @Override
public SeekPoints getSeekPoints(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
if (dataSize == C.LENGTH_UNSET) { if (dataSize == C.LENGTH_UNSET && !allowSeeksIfLengthUnknown) {
return new SeekPoints(new SeekPoint(0, firstFrameBytePosition)); return new SeekPoints(new SeekPoint(0, firstFrameBytePosition));
} }
long seekFramePosition = getFramePositionForTimeUs(timeUs); long seekFramePosition = getFramePositionForTimeUs(timeUs);
long seekTimeUs = getTimeUsAtPosition(seekFramePosition); long seekTimeUs = getTimeUsAtPosition(seekFramePosition);
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition); SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition);
if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) { // We only return a single seek point if the length is unknown, to avoid generating a second
// seek point beyond the end of the data in the case that the requested seek position is valid,
// but very close to the end of the content.
if (dataSize == C.LENGTH_UNSET
|| seekTimeUs >= timeUs
|| seekFramePosition + frameSize >= inputLength) {
return new SeekPoints(seekPoint); return new SeekPoints(seekPoint);
} else { } else {
long secondSeekPosition = seekFramePosition + frameSize; long secondSeekPosition = seekFramePosition + frameSize;
@ -118,8 +150,10 @@ public class ConstantBitrateSeekMap implements SeekMap {
long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE); long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE);
// Constrain to nearest preceding frame offset. // Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / frameSize) * frameSize; positionOffset = (positionOffset / frameSize) * frameSize;
positionOffset = if (dataSize != C.LENGTH_UNSET) {
Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize); positionOffset = min(positionOffset, dataSize - frameSize);
}
positionOffset = max(positionOffset, 0);
return firstFrameBytePosition + positionOffset; return firstFrameBytePosition + positionOffset;
} }
} }

View File

@ -20,6 +20,8 @@ import static com.google.android.exoplayer2.util.FileTypes.inferFileTypeFromUri;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.flac.FlacExtractor; import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
@ -125,6 +127,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
} }
private boolean constantBitrateSeekingEnabled; private boolean constantBitrateSeekingEnabled;
private boolean constantBitrateSeekingAlwaysEnabled;
@AdtsExtractor.Flags private int adtsFlags; @AdtsExtractor.Flags private int adtsFlags;
@AmrExtractor.Flags private int amrFlags; @AmrExtractor.Flags private int amrFlags;
@FlacExtractor.Flags private int flacFlags; @FlacExtractor.Flags private int flacFlags;
@ -158,6 +161,30 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
return this; return this;
} }
/**
* Convenience method to set whether approximate seeking using constant bitrate assumptions should
* be enabled for all extractors that support it, and if it should be enabled even if the content
* length (and hence the duration of the media) is unknown. If set to true, the flags required to
* enable this functionality will be OR'd with those passed to the setters when creating extractor
* instances. If set to false then the flags passed to the setters will be used without
* modification.
*
* <p>When seeking into content where the length is unknown, application code should ensure that
* requested seek positions are valid, or should be ready to handle playback failures reported
* through {@link Player.Listener#onPlayerError} with {@link PlaybackException#errorCode} set to
* {@link PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}.
*
* @param constantBitrateSeekingAlwaysEnabled Whether approximate seeking using a constant bitrate
* assumption should be enabled for all extractors that support it, including when the content
* duration is unknown.
* @return The factory, for convenience.
*/
public synchronized DefaultExtractorsFactory setConstantBitrateSeekingAlwaysEnabled(
boolean constantBitrateSeekingAlwaysEnabled) {
this.constantBitrateSeekingAlwaysEnabled = constantBitrateSeekingAlwaysEnabled;
return this;
}
/** /**
* Sets flags for {@link AdtsExtractor} instances created by the factory. * Sets flags for {@link AdtsExtractor} instances created by the factory.
* *
@ -333,6 +360,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
adtsFlags adtsFlags
| (constantBitrateSeekingEnabled | (constantBitrateSeekingEnabled
? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0)
| (constantBitrateSeekingAlwaysEnabled
? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS
: 0))); : 0)));
break; break;
case FileTypes.AMR: case FileTypes.AMR:
@ -341,6 +371,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
amrFlags amrFlags
| (constantBitrateSeekingEnabled | (constantBitrateSeekingEnabled
? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0)
| (constantBitrateSeekingAlwaysEnabled
? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS
: 0))); : 0)));
break; break;
case FileTypes.FLAC: case FileTypes.FLAC:
@ -367,6 +400,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
mp3Flags mp3Flags
| (constantBitrateSeekingEnabled | (constantBitrateSeekingEnabled
? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0)
| (constantBitrateSeekingAlwaysEnabled
? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS
: 0))); : 0)));
break; break;
case FileTypes.MP4: case FileTypes.MP4:

View File

@ -19,6 +19,8 @@ import androidx.annotation.IntDef;
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.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
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;
@ -52,20 +54,33 @@ public final class AmrExtractor implements Extractor {
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()}; public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()};
/** /**
* Flags controlling the behavior of the extractor. Possible flag value is {@link * Flags controlling the behavior of the extractor. Possible flag values are {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef( @IntDef(
flag = true, flag = true,
value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS})
public @interface Flags {} public @interface Flags {}
/** /**
* Flag to force enable seeking using a constant bitrate assumption in cases where seeking would * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
* otherwise not be possible. * otherwise not be possible.
*/ */
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
/**
* Like {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, except that seeking is also enabled in
* cases where the content length (and hence the duration of the media) is unknown. Application
* code should ensure that requested seek positions are valid when using this flag, or be ready to
* handle playback failures reported through {@link Player.Listener#onPlayerError} with {@link
* PlaybackException#errorCode} set to {@link
* PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}.
*
* <p>If this flag is set, then the behavior enabled by {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} is implicitly enabled as well.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS = 1 << 1;
/** /**
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR
@ -152,6 +167,9 @@ public final class AmrExtractor implements Extractor {
/** @param flags Flags that control the extractor's behavior. */ /** @param flags Flags that control the extractor's behavior. */
public AmrExtractor(@Flags int flags) { public AmrExtractor(@Flags int flags) {
if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0) {
flags |= FLAG_ENABLE_CONSTANT_BITRATE_SEEKING;
}
this.flags = flags; this.flags = flags;
scratch = new byte[1]; scratch = new byte[1];
firstSampleSize = C.LENGTH_UNSET; firstSampleSize = C.LENGTH_UNSET;
@ -360,15 +378,18 @@ public final class AmrExtractor implements Extractor {
hasOutputSeekMap = true; hasOutputSeekMap = true;
} else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD } else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD
|| sampleReadResult == RESULT_END_OF_INPUT) { || sampleReadResult == RESULT_END_OF_INPUT) {
seekMap = getConstantBitrateSeekMap(inputLength); seekMap =
getConstantBitrateSeekMap(
inputLength, (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0);
extractorOutput.seekMap(seekMap); extractorOutput.seekMap(seekMap);
hasOutputSeekMap = true; hasOutputSeekMap = true;
} }
} }
private SeekMap getConstantBitrateSeekMap(long inputLength) { private SeekMap getConstantBitrateSeekMap(long inputLength, boolean allowSeeksIfLengthUnknown) {
int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US); int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US);
return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize); return new ConstantBitrateSeekMap(
inputLength, firstSamplePosition, bitrate, firstSampleSize, allowSeeksIfLengthUnknown);
} }
@EnsuresNonNull({"extractorOutput", "trackOutput"}) @EnsuresNonNull({"extractorOutput", "trackOutput"})

View File

@ -28,13 +28,23 @@ import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFramePosition The position of the first frame in the stream. * @param firstFramePosition The position of the first frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the first frame. * @param mpegAudioHeader The MPEG audio header associated with the first frame.
* @param allowSeeksIfLengthUnknown Whether to allow seeking even if the length of the content is
* unknown.
*/ */
public ConstantBitrateSeeker( public ConstantBitrateSeeker(
long inputLength, long firstFramePosition, MpegAudioUtil.Header mpegAudioHeader) { long inputLength,
long firstFramePosition,
MpegAudioUtil.Header mpegAudioHeader,
boolean allowSeeksIfLengthUnknown) {
// Set the seeker frame size to the size of the first frame (even though some constant bitrate // Set the seeker frame size to the size of the first frame (even though some constant bitrate
// streams have variable frame sizes) to avoid the need to re-synchronize for constant frame // streams have variable frame sizes) to avoid the need to re-synchronize for constant frame
// size streams. // size streams.
super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize); super(
inputLength,
firstFramePosition,
mpegAudioHeader.bitrate,
mpegAudioHeader.frameSize,
allowSeeksIfLengthUnknown);
} }
@Override @Override

View File

@ -20,6 +20,8 @@ import androidx.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.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.audio.MpegAudioUtil;
import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
@ -56,8 +58,8 @@ public final class Mp3Extractor implements Extractor {
/** /**
* Flags controlling the behavior of the extractor. Possible flag values are {@link * Flags controlling the behavior of the extractor. Possible flag values are {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, {@link #FLAG_ENABLE_INDEX_SEEKING} and {@link * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS},
* #FLAG_DISABLE_ID3_METADATA}. * {@link #FLAG_ENABLE_INDEX_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -65,6 +67,7 @@ public final class Mp3Extractor implements Extractor {
flag = true, flag = true,
value = { value = {
FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_ENABLE_CONSTANT_BITRATE_SEEKING,
FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS,
FLAG_ENABLE_INDEX_SEEKING, FLAG_ENABLE_INDEX_SEEKING,
FLAG_DISABLE_ID3_METADATA FLAG_DISABLE_ID3_METADATA
}) })
@ -76,6 +79,21 @@ public final class Mp3Extractor implements Extractor {
* <p>This flag is ignored if {@link #FLAG_ENABLE_INDEX_SEEKING} is set. * <p>This flag is ignored if {@link #FLAG_ENABLE_INDEX_SEEKING} is set.
*/ */
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
/**
* Like {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, except that seeking is also enabled in
* cases where the content length (and hence the duration of the media) is unknown. Application
* code should ensure that requested seek positions are valid when using this flag, or be ready to
* handle playback failures reported through {@link Player.Listener#onPlayerError} with {@link
* PlaybackException#errorCode} set to {@link
* PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}.
*
* <p>If this flag is set, then the behavior enabled by {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} is implicitly enabled.
*
* <p>This flag is ignored if {@link #FLAG_ENABLE_INDEX_SEEKING} is set.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS = 1 << 1;
/** /**
* Flag to force index seeking, in which a time-to-byte mapping is built as the file is read. * Flag to force index seeking, in which a time-to-byte mapping is built as the file is read.
* *
@ -88,12 +106,12 @@ public final class Mp3Extractor implements Extractor {
* provide precise enough seeking metadata. * provide precise enough seeking metadata.
* </ul> * </ul>
*/ */
public static final int FLAG_ENABLE_INDEX_SEEKING = 1 << 1; public static final int FLAG_ENABLE_INDEX_SEEKING = 1 << 2;
/** /**
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required. * required.
*/ */
public static final int FLAG_DISABLE_ID3_METADATA = 1 << 2; public static final int FLAG_DISABLE_ID3_METADATA = 1 << 3;
/** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */ /** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */
private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE = private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE =
@ -158,6 +176,9 @@ public final class Mp3Extractor implements Extractor {
* C#TIME_UNSET} if forcing is not required. * C#TIME_UNSET} if forcing is not required.
*/ */
public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {
if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0) {
flags |= FLAG_ENABLE_CONSTANT_BITRATE_SEEKING;
}
this.flags = flags; this.flags = flags;
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(SCRATCH_LENGTH); scratch = new ParsableByteArray(SCRATCH_LENGTH);
@ -446,7 +467,9 @@ public final class Mp3Extractor implements Extractor {
if (resultSeeker == null if (resultSeeker == null
|| (!resultSeeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { || (!resultSeeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
resultSeeker = getConstantBitrateSeeker(input); resultSeeker =
getConstantBitrateSeeker(
input, (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0);
} }
return resultSeeker; return resultSeeker;
@ -485,7 +508,7 @@ public final class Mp3Extractor implements Extractor {
input.skipFully(synchronizedHeader.frameSize); input.skipFully(synchronizedHeader.frameSize);
if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) { if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) {
// Fall back to constant bitrate seeking for Info headers missing a table of contents. // Fall back to constant bitrate seeking for Info headers missing a table of contents.
return getConstantBitrateSeeker(input); return getConstantBitrateSeeker(input, /* allowSeeksIfLengthUnknown= */ false);
} }
} else if (seekHeader == SEEK_HEADER_VBRI) { } else if (seekHeader == SEEK_HEADER_VBRI) {
seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
@ -499,11 +522,13 @@ public final class Mp3Extractor implements Extractor {
} }
/** Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. */ /** Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. */
private Seeker getConstantBitrateSeeker(ExtractorInput input) throws IOException { private Seeker getConstantBitrateSeeker(ExtractorInput input, boolean allowSeeksIfLengthUnknown)
throws IOException {
input.peekFully(scratch.getData(), 0, 4); input.peekFully(scratch.getData(), 0, 4);
scratch.setPosition(0); scratch.setPosition(0);
synchronizedHeader.setForHeaderData(scratch.readInt()); synchronizedHeader.setForHeaderData(scratch.readInt());
return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); return new ConstantBitrateSeeker(
input.getLength(), input.getPosition(), synchronizedHeader, allowSeeksIfLengthUnknown);
} }
@EnsuresNonNull({"extractorOutput", "realTrackOutput"}) @EnsuresNonNull({"extractorOutput", "realTrackOutput"})

View File

@ -22,6 +22,8 @@ import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
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;
@ -48,14 +50,15 @@ public final class AdtsExtractor implements Extractor {
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()}; public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()};
/** /**
* Flags controlling the behavior of the extractor. Possible flag value is {@link * Flags controlling the behavior of the extractor. Possible flag values are {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef( @IntDef(
flag = true, flag = true,
value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS})
public @interface Flags {} public @interface Flags {}
/** /**
* Flag to force enable seeking using a constant bitrate assumption in cases where seeking would * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
@ -65,6 +68,18 @@ public final class AdtsExtractor implements Extractor {
* are not precise, especially when the stream bitrate varies a lot. * are not precise, especially when the stream bitrate varies a lot.
*/ */
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
/**
* Like {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, except that seeking is also enabled in
* cases where the content length (and hence the duration of the media) is unknown. Application
* code should ensure that requested seek positions are valid when using this flag, or be ready to
* handle playback failures reported through {@link Player.Listener#onPlayerError} with {@link
* PlaybackException#errorCode} set to {@link
* PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}.
*
* <p>If this flag is set, then the behavior enabled by {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} is implicitly enabled as well.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS = 1 << 1;
private static final int MAX_PACKET_SIZE = 2 * 1024; private static final int MAX_PACKET_SIZE = 2 * 1024;
/** /**
@ -105,6 +120,9 @@ public final class AdtsExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior. * @param flags Flags that control the extractor's behavior.
*/ */
public AdtsExtractor(@Flags int flags) { public AdtsExtractor(@Flags int flags) {
if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0) {
flags |= FLAG_ENABLE_CONSTANT_BITRATE_SEEKING;
}
this.flags = flags; this.flags = flags;
reader = new AdtsReader(true); reader = new AdtsReader(true);
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
@ -182,14 +200,16 @@ public final class AdtsExtractor implements Extractor {
long inputLength = input.getLength(); long inputLength = input.getLength();
boolean canUseConstantBitrateSeeking = boolean canUseConstantBitrateSeeking =
(flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET; (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0
|| ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0
&& inputLength != C.LENGTH_UNSET);
if (canUseConstantBitrateSeeking) { if (canUseConstantBitrateSeeking) {
calculateAverageFrameSize(input); calculateAverageFrameSize(input);
} }
int bytesRead = input.read(packetBuffer.getData(), 0, MAX_PACKET_SIZE); int bytesRead = input.read(packetBuffer.getData(), 0, MAX_PACKET_SIZE);
boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT; boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT;
maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream); maybeOutputSeekMap(inputLength, readEndOfStream);
if (readEndOfStream) { if (readEndOfStream) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
@ -231,12 +251,13 @@ public final class AdtsExtractor implements Extractor {
} }
@RequiresNonNull("extractorOutput") @RequiresNonNull("extractorOutput")
private void maybeOutputSeekMap( private void maybeOutputSeekMap(long inputLength, boolean readEndOfStream) {
long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) {
if (hasOutputSeekMap) { if (hasOutputSeekMap) {
return; return;
} }
boolean useConstantBitrateSeeking = canUseConstantBitrateSeeking && averageFrameSize > 0;
boolean useConstantBitrateSeeking =
(flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && averageFrameSize > 0;
if (useConstantBitrateSeeking if (useConstantBitrateSeeking
&& reader.getSampleDurationUs() == C.TIME_UNSET && reader.getSampleDurationUs() == C.TIME_UNSET
&& !readEndOfStream) { && !readEndOfStream) {
@ -246,7 +267,9 @@ public final class AdtsExtractor implements Extractor {
} }
if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) { if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) {
extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength)); extractorOutput.seekMap(
getConstantBitrateSeekMap(
inputLength, (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0));
} else { } else {
extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
} }
@ -314,9 +337,10 @@ public final class AdtsExtractor implements Extractor {
hasCalculatedAverageFrameSize = true; hasCalculatedAverageFrameSize = true;
} }
private SeekMap getConstantBitrateSeekMap(long inputLength) { private SeekMap getConstantBitrateSeekMap(long inputLength, boolean allowSeeksIfLengthUnknown) {
int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs()); int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs());
return new ConstantBitrateSeekMap(inputLength, firstFramePosition, bitrate, averageFrameSize); return new ConstantBitrateSeekMap(
inputLength, firstFramePosition, bitrate, averageFrameSize, allowSeeksIfLengthUnknown);
} }
/** /**