Expect PresentationTime Discontinuity During Stream Transitions
PiperOrigin-RevId: 440378974
This commit is contained in:
parent
20547c3392
commit
c007068ddb
@ -585,8 +585,9 @@ public final class C {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
|
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
|
||||||
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
|
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_FIRST_SAMPLE},
|
||||||
* {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
|
* {@link #BUFFER_FLAG_LAST_SAMPLE}, {@link #BUFFER_FLAG_ENCRYPTED} and {@link
|
||||||
|
* #BUFFER_FLAG_DECODE_ONLY}.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Documented
|
@Documented
|
||||||
@ -597,6 +598,7 @@ public final class C {
|
|||||||
value = {
|
value = {
|
||||||
BUFFER_FLAG_KEY_FRAME,
|
BUFFER_FLAG_KEY_FRAME,
|
||||||
BUFFER_FLAG_END_OF_STREAM,
|
BUFFER_FLAG_END_OF_STREAM,
|
||||||
|
BUFFER_FLAG_FIRST_SAMPLE,
|
||||||
BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA,
|
BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA,
|
||||||
BUFFER_FLAG_LAST_SAMPLE,
|
BUFFER_FLAG_LAST_SAMPLE,
|
||||||
BUFFER_FLAG_ENCRYPTED,
|
BUFFER_FLAG_ENCRYPTED,
|
||||||
@ -608,6 +610,8 @@ public final class C {
|
|||||||
/** Flag for empty buffers that signal that the end of the stream was reached. */
|
/** Flag for empty buffers that signal that the end of the stream was reached. */
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
||||||
|
/** Indicates that a buffer is known to contain the first media sample of the stream. */
|
||||||
|
@UnstableApi public static final int BUFFER_FLAG_FIRST_SAMPLE = 1 << 27; // 0x08000000
|
||||||
/** Indicates that a buffer has supplemental data. */
|
/** Indicates that a buffer has supplemental data. */
|
||||||
@UnstableApi public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000
|
@UnstableApi public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000
|
||||||
/** Indicates that a buffer is known to contain the last media sample of the stream. */
|
/** Indicates that a buffer is known to contain the last media sample of the stream. */
|
||||||
|
@ -34,6 +34,11 @@ public abstract class Buffer {
|
|||||||
return getFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
return getFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns whether the {@link C#BUFFER_FLAG_FIRST_SAMPLE} flag is set. */
|
||||||
|
public final boolean isFirstSample() {
|
||||||
|
return getFlag(C.BUFFER_FLAG_FIRST_SAMPLE);
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns whether the {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set. */
|
/** Returns whether the {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set. */
|
||||||
public final boolean isEndOfStream() {
|
public final boolean isEndOfStream() {
|
||||||
return getFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
return getFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
||||||
|
@ -235,6 +235,9 @@ public abstract class SimpleDecoder<
|
|||||||
if (inputBuffer.isDecodeOnly()) {
|
if (inputBuffer.isDecodeOnly()) {
|
||||||
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||||
}
|
}
|
||||||
|
if (inputBuffer.isFirstSample()) {
|
||||||
|
outputBuffer.addFlag(C.BUFFER_FLAG_FIRST_SAMPLE);
|
||||||
|
}
|
||||||
@Nullable E exception;
|
@Nullable E exception;
|
||||||
try {
|
try {
|
||||||
exception = decode(inputBuffer, outputBuffer, resetDecoder);
|
exception = decode(inputBuffer, outputBuffer, resetDecoder);
|
||||||
|
@ -130,6 +130,7 @@ public abstract class DecoderAudioRenderer<
|
|||||||
private int encoderPadding;
|
private int encoderPadding;
|
||||||
|
|
||||||
private boolean experimentalKeepAudioTrackOnSeek;
|
private boolean experimentalKeepAudioTrackOnSeek;
|
||||||
|
private boolean firstStreamSampleRead;
|
||||||
|
|
||||||
@Nullable private T decoder;
|
@Nullable private T decoder;
|
||||||
|
|
||||||
@ -389,6 +390,9 @@ public abstract class DecoderAudioRenderer<
|
|||||||
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
||||||
audioSink.handleDiscontinuity();
|
audioSink.handleDiscontinuity();
|
||||||
}
|
}
|
||||||
|
if (outputBuffer.isFirstSample()) {
|
||||||
|
audioSink.handleDiscontinuity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputBuffer.isEndOfStream()) {
|
if (outputBuffer.isEndOfStream()) {
|
||||||
@ -470,6 +474,10 @@ public abstract class DecoderAudioRenderer<
|
|||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!firstStreamSampleRead) {
|
||||||
|
firstStreamSampleRead = true;
|
||||||
|
inputBuffer.addFlag(C.BUFFER_FLAG_FIRST_SAMPLE);
|
||||||
|
}
|
||||||
inputBuffer.flip();
|
inputBuffer.flip();
|
||||||
inputBuffer.format = inputFormat;
|
inputBuffer.format = inputFormat;
|
||||||
onQueueInputBuffer(inputBuffer);
|
onQueueInputBuffer(inputBuffer);
|
||||||
@ -587,6 +595,13 @@ public abstract class DecoderAudioRenderer<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
super.onStreamChanged(formats, startPositionUs, offsetUs);
|
||||||
|
firstStreamSampleRead = false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(@MessageType int messageType, @Nullable Object message)
|
public void handleMessage(@MessageType int messageType, @Nullable Object message)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
|
@ -887,7 +887,6 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleDiscontinuity() {
|
public void handleDiscontinuity() {
|
||||||
// Force resynchronization after a skipped buffer.
|
|
||||||
startMediaTimeUsNeedsSync = true;
|
startMediaTimeUsNeedsSync = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,12 @@ import static androidx.media3.exoplayer.RendererCapabilities.DECODER_SUPPORT_PRI
|
|||||||
import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_NOT_SUPPORTED;
|
import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_NOT_SUPPORTED;
|
||||||
import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_SUPPORTED;
|
import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_SUPPORTED;
|
||||||
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
||||||
|
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@ -46,6 +51,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.InOrder;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
@ -139,6 +145,100 @@ public class DecoderAudioRendererTest {
|
|||||||
verify(mockAudioSink, times(1)).reset();
|
verify(mockAudioSink, times(1)).reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstSampleOfStreamSignalsDiscontinuityToAudioSink() throws Exception {
|
||||||
|
when(mockAudioSink.handleBuffer(any(), anyLong(), anyInt())).thenReturn(true);
|
||||||
|
when(mockAudioSink.isEnded()).thenReturn(true);
|
||||||
|
InOrder inOrderAudioSink = inOrder(mockAudioSink);
|
||||||
|
FakeSampleStream fakeSampleStream =
|
||||||
|
new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
FORMAT,
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME),
|
||||||
|
oneByteSample(/* timeUs= */ 1_000),
|
||||||
|
END_OF_STREAM_ITEM));
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
audioRenderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {FORMAT},
|
||||||
|
fakeSampleStream,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0);
|
||||||
|
|
||||||
|
audioRenderer.setCurrentStreamFinal();
|
||||||
|
while (!audioRenderer.isEnded()) {
|
||||||
|
audioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inOrderAudioSink.verify(mockAudioSink, times(1)).handleDiscontinuity();
|
||||||
|
inOrderAudioSink.verify(mockAudioSink, times(2)).handleBuffer(any(), anyLong(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstSampleOfReplacementStreamSignalsDiscontinuityToAudioSink() throws Exception {
|
||||||
|
when(mockAudioSink.handleBuffer(any(), anyLong(), anyInt())).thenReturn(true);
|
||||||
|
when(mockAudioSink.isEnded()).thenReturn(true);
|
||||||
|
InOrder inOrderAudioSink = inOrder(mockAudioSink);
|
||||||
|
FakeSampleStream fakeSampleStream1 =
|
||||||
|
new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
FORMAT,
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME),
|
||||||
|
oneByteSample(/* timeUs= */ 1_000),
|
||||||
|
END_OF_STREAM_ITEM));
|
||||||
|
fakeSampleStream1.writeData(/* startPositionUs= */ 0);
|
||||||
|
FakeSampleStream fakeSampleStream2 =
|
||||||
|
new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
FORMAT,
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 1_000_000, C.BUFFER_FLAG_KEY_FRAME),
|
||||||
|
oneByteSample(/* timeUs= */ 1_001_000),
|
||||||
|
END_OF_STREAM_ITEM));
|
||||||
|
fakeSampleStream2.writeData(/* startPositionUs= */ 0);
|
||||||
|
audioRenderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {FORMAT},
|
||||||
|
fakeSampleStream1,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0);
|
||||||
|
|
||||||
|
while (!audioRenderer.hasReadStreamToEnd()) {
|
||||||
|
audioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
audioRenderer.replaceStream(
|
||||||
|
new Format[] {FORMAT},
|
||||||
|
fakeSampleStream2,
|
||||||
|
/* startPositionUs= */ 1_000_000,
|
||||||
|
/* offsetUs= */ 1_000_000);
|
||||||
|
audioRenderer.setCurrentStreamFinal();
|
||||||
|
while (!audioRenderer.isEnded()) {
|
||||||
|
audioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inOrderAudioSink.verify(mockAudioSink, times(1)).handleDiscontinuity();
|
||||||
|
inOrderAudioSink.verify(mockAudioSink, times(2)).handleBuffer(any(), anyLong(), anyInt());
|
||||||
|
inOrderAudioSink.verify(mockAudioSink, times(1)).handleDiscontinuity();
|
||||||
|
inOrderAudioSink.verify(mockAudioSink, times(2)).handleBuffer(any(), anyLong(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
private static final class FakeDecoder
|
private static final class FakeDecoder
|
||||||
extends SimpleDecoder<DecoderInputBuffer, SimpleDecoderOutputBuffer, DecoderException> {
|
extends SimpleDecoder<DecoderInputBuffer, SimpleDecoderOutputBuffer, DecoderException> {
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
discontinuity:
|
||||||
config:
|
config:
|
||||||
pcmEncoding = 2
|
pcmEncoding = 2
|
||||||
channelCount = 2
|
channelCount = 2
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
discontinuity:
|
||||||
config:
|
config:
|
||||||
pcmEncoding = 536870912
|
pcmEncoding = 536870912
|
||||||
channelCount = 2
|
channelCount = 2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user