diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java index 3eef49c42c..e68830f60d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -580,8 +580,9 @@ public final class C { /** * 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}, - * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}. + * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_FIRST_SAMPLE}, + * {@link #BUFFER_FLAG_LAST_SAMPLE}, {@link #BUFFER_FLAG_ENCRYPTED} and {@link + * #BUFFER_FLAG_DECODE_ONLY}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -591,6 +592,7 @@ public final class C { value = { BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM, + BUFFER_FLAG_FIRST_SAMPLE, BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA, BUFFER_FLAG_LAST_SAMPLE, BUFFER_FLAG_ENCRYPTED, @@ -601,6 +603,8 @@ public final class C { public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME; /** Flag for empty buffers that signal that the end of the stream was reached. */ 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. */ + public static final int BUFFER_FLAG_FIRST_SAMPLE = 1 << 27; // 0x08000000 /** Indicates that a buffer has supplemental data. */ 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. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java index 53e9d50d90..9a13f09019 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java @@ -126,6 +126,7 @@ public abstract class DecoderAudioRenderer< private int encoderPadding; private boolean experimentalKeepAudioTrackOnSeek; + private boolean firstStreamSampleRead; @Nullable private T decoder; @@ -385,6 +386,9 @@ public abstract class DecoderAudioRenderer< decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; audioSink.handleDiscontinuity(); } + if (outputBuffer.isFirstSample()) { + audioSink.handleDiscontinuity(); + } } if (outputBuffer.isEndOfStream()) { @@ -466,6 +470,10 @@ public abstract class DecoderAudioRenderer< inputBuffer = null; return false; } + if (!firstStreamSampleRead) { + firstStreamSampleRead = true; + inputBuffer.addFlag(C.BUFFER_FLAG_FIRST_SAMPLE); + } inputBuffer.flip(); inputBuffer.format = inputFormat; onQueueInputBuffer(inputBuffer); @@ -583,6 +591,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 public void handleMessage(@MessageType int messageType, @Nullable Object message) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index db04bdeb69..6ee3f04204 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -878,7 +878,6 @@ public final class DefaultAudioSink implements AudioSink { @Override public void handleDiscontinuity() { - // Force resynchronization after a skipped buffer. startMediaTimeUsNeedsSync = true; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java index 34a9761318..9b3e0a5584 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java @@ -21,7 +21,12 @@ import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED; import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_SUPPORTED; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; +import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; 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.verify; import static org.mockito.Mockito.when; @@ -46,6 +51,7 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.annotation.Config; @@ -139,6 +145,100 @@ public class DecoderAudioRendererTest { 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 extends SimpleDecoder { diff --git a/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java b/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java index bf54ce5a70..43a3f18469 100644 --- a/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java +++ b/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java @@ -32,6 +32,11 @@ public abstract class Buffer { 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. */ public final boolean isEndOfStream() { return getFlag(C.BUFFER_FLAG_END_OF_STREAM); diff --git a/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index 4ac32f685f..2c7f302369 100644 --- a/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/decoder/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -233,6 +233,9 @@ public abstract class SimpleDecoder< if (inputBuffer.isDecodeOnly()) { outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); } + if (inputBuffer.isFirstSample()) { + outputBuffer.addFlag(C.BUFFER_FLAG_FIRST_SAMPLE); + } @Nullable E exception; try { exception = decode(inputBuffer, outputBuffer, resetDecoder); diff --git a/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-16bit.mka.audiosink.dump b/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-16bit.mka.audiosink.dump index b7319b872b..bc71f0a24b 100644 --- a/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-16bit.mka.audiosink.dump +++ b/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-16bit.mka.audiosink.dump @@ -1,3 +1,4 @@ +discontinuity: config: pcmEncoding = 2 channelCount = 2 diff --git a/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-24bit.mka.audiosink.dump b/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-24bit.mka.audiosink.dump index f425e7e2f2..cc55bb6e9a 100644 --- a/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-24bit.mka.audiosink.dump +++ b/testdata/src/test/assets/audiosinkdumps/mka/bear-flac-24bit.mka.audiosink.dump @@ -1,3 +1,4 @@ +discontinuity: config: pcmEncoding = 536870912 channelCount = 2