Add Decoder.setOutputStartTimeUs and use it in extension decoders

This gets rid of the reliance on the decode only flag that is still
set on input buffers to the decoder if they are less than the start
time.

We still need to set and check the decode-only flag in SimpleDecoder
to ensure compatbility with custom decoders that use the flag while
it's not fully removed.

PiperOrigin-RevId: 570736692
This commit is contained in:
tonihei 2023-10-04 10:35:15 -07:00 committed by Copybara-Service
parent 64fe863f31
commit a03e20fe6c
10 changed files with 72 additions and 10 deletions

View File

@ -33,11 +33,14 @@
* Allow multiple of the same DASH identifier in segment template url.
* Smooth Streaming Extension:
* RTSP Extension:
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Add `DecoderOutputBuffer.shouldBeSkipped` to directly mark output
buffers that don't need to be presented. This is preferred over
`C.BUFFER_FLAG_DECODE_ONLY`.
* MIDI extension:
`C.BUFFER_FLAG_DECODE_ONLY` that will be deprecated.
* Add `Decoder.setOutputStartTimeUs` and
`SimpleDecoder.isAtLeastOutputStartTimeUs` to allow decoders to drop
decode-only samples before the start time. This should be preferred to
`Buffer.isDecodeOnly` that will be deprecated.
* Leanback extension:
* Cast Extension:
* Test Utilities:

View File

@ -35,6 +35,19 @@ public interface Decoder<I, O, E extends DecoderException> {
*/
String getName();
/**
* Sets the timestamp from which output buffers should be produced, in microseconds.
*
* <p>Any decoded buffer with a timestamp less than {@code outputStartTimeUs} should be skipped by
* the implementation and not made available via {@link #dequeueOutputBuffer}.
*
* <p>This method must only be called before {@linkplain #queueInputBuffer queuing the first input
* buffer} initially or after {@link #flush()}.
*
* @param outputStartTimeUs The time from which output buffer should be produced, in microseconds.
*/
void setOutputStartTimeUs(long outputStartTimeUs);
/**
* Dequeues the next input buffer to be filled and queued to the decoder.
*

View File

@ -48,6 +48,7 @@ public abstract class SimpleDecoder<
private boolean flushed;
private boolean released;
private int skippedOutputBufferCount;
private long outputStartTimeUs;
/**
* @param inputBuffers An array of nulls that will be used to store references to input buffers.
@ -56,6 +57,7 @@ public abstract class SimpleDecoder<
@SuppressWarnings("nullness:method.invocation")
protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) {
lock = new Object();
outputStartTimeUs = C.TIME_UNSET;
queuedInputBuffers = new ArrayDeque<>();
queuedOutputBuffers = new ArrayDeque<>();
availableInputBuffers = inputBuffers;
@ -93,6 +95,30 @@ public abstract class SimpleDecoder<
}
}
/**
* Returns whether a sample time is greater or equal to the {@link #setOutputStartTimeUs output
* start time}, if set.
*
* <p>If this method returns false, the buffer will not be made available as an output buffer.
*
* @param timeUs The buffer time, in microseconds.
* @return Whether the buffer time is greater or equal to the output start time, or {@code true}
* if the output start time is not set.
*/
protected final boolean isAtLeastOutputStartTimeUs(long timeUs) {
synchronized (lock) {
return outputStartTimeUs == C.TIME_UNSET || timeUs >= outputStartTimeUs;
}
}
@Override
public final void setOutputStartTimeUs(long outputStartTimeUs) {
synchronized (lock) {
Assertions.checkState(availableInputBufferCount == availableInputBuffers.length || flushed);
this.outputStartTimeUs = outputStartTimeUs;
}
}
@Override
@Nullable
public final I dequeueInputBuffer() throws E {
@ -233,7 +259,7 @@ public abstract class SimpleDecoder<
outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
} else {
outputBuffer.timeUs = inputBuffer.timeUs;
if (inputBuffer.isDecodeOnly()) {
if (!isAtLeastOutputStartTimeUs(inputBuffer.timeUs) || inputBuffer.isDecodeOnly()) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
if (inputBuffer.isFirstSample()) {
@ -263,7 +289,9 @@ public abstract class SimpleDecoder<
synchronized (lock) {
if (flushed) {
outputBuffer.release();
} else if (outputBuffer.isDecodeOnly() || outputBuffer.shouldBeSkipped) {
} else if ((!outputBuffer.isEndOfStream() && !isAtLeastOutputStartTimeUs(outputBuffer.timeUs))
|| outputBuffer.isDecodeOnly()
|| outputBuffer.shouldBeSkipped) {
skippedOutputBufferCount++;
outputBuffer.release();
} else {

View File

@ -105,7 +105,7 @@ public final class Gav1Decoder
"gav1Decode error: " + gav1GetErrorMessage(gav1DecoderContext));
}
boolean decodeOnly = inputBuffer.isDecodeOnly();
boolean decodeOnly = !isAtLeastOutputStartTimeUs(inputBuffer.timeUs);
if (!decodeOnly) {
outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
}

View File

@ -143,8 +143,9 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
if (lastReceivedTimestampUs == C.TIME_UNSET) {
outputTimeUs = inputBuffer.timeUs;
}
boolean isDecodeOnly = !isAtLeastOutputStartTimeUs(inputBuffer.timeUs);
try {
if (!inputBuffer.isDecodeOnly()) {
if (!isDecodeOnly) {
// Yield the thread to the Synthesizer to produce PCM samples up to this buffer's timestamp.
if (lastReceivedTimestampUs != C.TIME_UNSET) {
double timeToSleepSecs = (inputBuffer.timeUs - lastReceivedTimestampUs) * 0.000001D;
@ -167,7 +168,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
int availableSamples = reader.available();
// Ensure there are no remaining bytes if the input buffer is decode only.
checkState(!inputBuffer.isDecodeOnly() || reader.available() == 0);
checkState(!isDecodeOnly || availableSamples == 0);
if (availableSamples > audioStreamOutputBuffer.length) {
// Increase the size of the buffer by 25% of the availableSamples (arbitrary number).

View File

@ -165,7 +165,7 @@ public final class VpxDecoder
}
}
if (!inputBuffer.isDecodeOnly()) {
if (isAtLeastOutputStartTimeUs(inputBuffer.timeUs)) {
outputBuffer.init(inputBuffer.timeUs, outputMode, lastSupplementalData);
int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer);
if (getFrameResult == 1) {

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_DRM_SESSION_CHANGED;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_REUSE_NOT_IMPLEMENTED;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO;
@ -557,7 +558,9 @@ public abstract class DecoderAudioRenderer<
outputBuffer.release();
outputBuffer = null;
}
Decoder<?, ?, ?> decoder = checkNotNull(this.decoder);
decoder.flush();
decoder.setOutputStartTimeUs(getLastResetPositionUs());
decoderReceivedBuffers = false;
}
}
@ -735,6 +738,7 @@ public abstract class DecoderAudioRenderer<
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createAudioDecoder");
decoder = createDecoder(inputFormat, cryptoConfig);
decoder.setOutputStartTimeUs(getLastResetPositionUs());
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
eventDispatcher.decoderInitialized(

View File

@ -86,6 +86,11 @@ public final class ExoplayerCuesDecoder implements SubtitleDecoder {
return "ExoplayerCuesDecoder";
}
@Override
public void setOutputStartTimeUs(long outputStartTimeUs) {
// Do nothing.
}
@Nullable
@Override
public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException {

View File

@ -366,7 +366,9 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
outputBuffer.release();
outputBuffer = null;
}
checkNotNull(decoder).flush();
Decoder<?, ?, ?> decoder = checkNotNull(this.decoder);
decoder.flush();
decoder.setOutputStartTimeUs(getLastResetPositionUs());
decoderReceivedBuffers = false;
}
}
@ -718,6 +720,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
try {
long decoderInitializingTimestamp = SystemClock.elapsedRealtime();
decoder = createDecoder(checkNotNull(inputFormat), cryptoConfig);
decoder.setOutputStartTimeUs(getLastResetPositionUs());
setDecoderOutputMode(outputMode);
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
eventDispatcher.decoderInitialized(

View File

@ -58,6 +58,11 @@ import java.util.PriorityQueue;
@Override
public abstract String getName();
@Override
public final void setOutputStartTimeUs(long outputStartTimeUs) {
// Do nothing.
}
@Override
public void setPositionUs(long positionUs) {
playbackPositionUs = positionUs;