diff --git a/build.gradle b/build.gradle index d520925fb0..c62a97b6e3 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ allprojects { repositories { google() jcenter() + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } project.ext { exoplayerPublishEnabled = false diff --git a/constants.gradle b/constants.gradle index 1a7840588f..f3bebf6038 100644 --- a/constants.gradle +++ b/constants.gradle @@ -23,7 +23,7 @@ project.ext { junitVersion = '4.13-rc-2' guavaVersion = '28.2-android' mockitoVersion = '2.25.0' - robolectricVersion = '4.3.1' + robolectricVersion = '4.4-SNAPSHOT' checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' kotlinAnnotationsVersion = '1.3.70' diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java new file mode 100644 index 0000000000..48fbdf5564 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 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.audio; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.SystemClock; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; +import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link MediaCodecAudioRenderer} */ +@RunWith(AndroidJUnit4.class) +public class MediaCodecAudioRendererTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private static final Format AUDIO_AAC = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setPcmEncoding(C.ENCODING_PCM_16BIT) + .setChannelCount(2) + .setSampleRate(44100) + .setEncoderDelay(100) + .setEncoderPadding(150) + .build(); + + private MediaCodecAudioRenderer mediaCodecAudioRenderer; + + @Mock private AudioSink audioSink; + + @Before + public void setUp() throws Exception { + // audioSink isEnded can always be true because the MediaCodecAudioRenderer isEnded = + // super.isEnded && audioSink.isEnded. + when(audioSink.isEnded()).thenReturn(true); + + when(audioSink.handleBuffer(any(), anyLong(), anyInt())).thenReturn(true); + + MediaCodecSelector mediaCodecSelector = + new MediaCodecSelector() { + @Override + public List getDecoderInfos( + String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) { + return Collections.singletonList( + MediaCodecInfo.newInstance( + /* name= */ "name", + /* mimeType= */ mimeType, + /* codecMimeType= */ mimeType, + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + } + }; + + mediaCodecAudioRenderer = + new MediaCodecAudioRenderer( + ApplicationProvider.getApplicationContext(), + mediaCodecSelector, + /* enableDecoderFallback= */ false, + /* eventHandler= */ null, + /* eventListener= */ null, + audioSink) { + @Override + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + throws DecoderQueryException { + return RendererCapabilities.create(FORMAT_HANDLED); + } + }; + } + + @Test + public void render_configuresAudioSink() throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ AUDIO_AAC, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + + mediaCodecAudioRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {AUDIO_AAC}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* offsetUs */ 0); + + mediaCodecAudioRenderer.start(); + mediaCodecAudioRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + mediaCodecAudioRenderer.render(/* positionUs= */ 250, SystemClock.elapsedRealtime() * 1000); + mediaCodecAudioRenderer.setCurrentStreamFinal(); + + int positionUs = 500; + do { + mediaCodecAudioRenderer.render(positionUs, SystemClock.elapsedRealtime() * 1000); + positionUs += 250; + } while (!mediaCodecAudioRenderer.isEnded()); + + verify(audioSink) + .configure( + AUDIO_AAC.pcmEncoding, + AUDIO_AAC.channelCount, + AUDIO_AAC.sampleRate, + /* specifiedBufferSize= */ 0, + /* outputChannels= */ null, + AUDIO_AAC.encoderDelay, + AUDIO_AAC.encoderPadding); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java new file mode 100644 index 0000000000..4ec7bd8043 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2020 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.video; + +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.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.graphics.SurfaceTexture; +import android.os.Handler; +import android.os.SystemClock; +import android.view.Surface; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; +import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.LooperMode; + +/** Unit test for {@link MediaCodecVideoRenderer}. */ +@RunWith(AndroidJUnit4.class) +@LooperMode(LooperMode.Mode.LEGACY) +public class MediaCodecVideoRendererTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private static final Format VIDEO_H264 = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .build(); + + private MediaCodecVideoRenderer mediaCodecVideoRenderer; + @Nullable private Format currentOutputFormat; + + @Mock private VideoRendererEventListener eventListener; + + @Before + public void setUp() throws Exception { + MediaCodecSelector mediaCodecSelector = + new MediaCodecSelector() { + @Override + public List getDecoderInfos( + String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) { + return Collections.singletonList( + MediaCodecInfo.newInstance( + /* name= */ "name", + /* mimeType= */ mimeType, + /* codecMimeType= */ mimeType, + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + } + }; + + mediaCodecVideoRenderer = + new MediaCodecVideoRenderer( + ApplicationProvider.getApplicationContext(), + mediaCodecSelector, + /* allowedJoiningTimeMs= */ 0, + /* eventHandler= */ new Handler(), + /* eventListener= */ eventListener, + /* maxDroppedFramesToNotify= */ 1) { + @Override + @Capabilities + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + throws DecoderQueryException { + return RendererCapabilities.create(FORMAT_HANDLED); + } + + @Override + protected void onOutputFormatChanged(Format outputFormat) { + super.onOutputFormatChanged(outputFormat); + currentOutputFormat = outputFormat; + } + }; + + mediaCodecVideoRenderer.handleMessage( + Renderer.MSG_SET_SURFACE, new Surface(new SurfaceTexture(/* texName= */ 0))); + } + + @Test + public void render_dropsLateBuffer() throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50_000, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), // First buffer. + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), // Late buffer. + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), // Last buffer. + FakeSampleStreamItem.END_OF_STREAM_ITEM); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); + mediaCodecVideoRenderer.render(40_000, SystemClock.elapsedRealtime() * 1000); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + int posUs = 80_001; // Ensures buffer will be 30_001us late. + while (!mediaCodecVideoRenderer.isEnded()) { + mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); + posUs += 40_000; + } + + verify(eventListener).onDroppedFrames(eq(1), anyLong()); + } + + @Test + public void render_sendsVideoSizeChangeWithCurrentFormatValues() throws Exception { + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 0, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM), + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + mediaCodecVideoRenderer.start(); + + int positionUs = 0; + do { + mediaCodecVideoRenderer.render(positionUs, SystemClock.elapsedRealtime() * 1000); + positionUs += 10; + } while (!mediaCodecVideoRenderer.isEnded()); + + verify(eventListener) + .onVideoSizeChanged( + VIDEO_H264.width, + VIDEO_H264.height, + VIDEO_H264.rotationDegrees, + VIDEO_H264.pixelWidthHeightRatio); + } + + @Test + public void + render_withMultipleQueued_sendsVideoSizeChangedWithCorrectPixelAspectRatioWhenMultipleQueued() + throws Exception { + Format pAsp1 = VIDEO_H264.buildUpon().setPixelWidthHeightRatio(1f).build(); + Format pAsp2 = VIDEO_H264.buildUpon().setPixelWidthHeightRatio(2f).build(); + Format pAsp3 = VIDEO_H264.buildUpon().setPixelWidthHeightRatio(3f).build(); + + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ pAsp1, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 5000, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {pAsp1, pAsp2, pAsp3}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* offsetUs */ 0); + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + mediaCodecVideoRenderer.render(/* positionUs= */ 250, SystemClock.elapsedRealtime() * 1000); + + fakeSampleStream.addFakeSampleStreamItem(new FakeSampleStreamItem(pAsp2)); + fakeSampleStream.addFakeSampleStreamItem( + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + fakeSampleStream.addFakeSampleStreamItem( + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + fakeSampleStream.addFakeSampleStreamItem(new FakeSampleStreamItem(pAsp3)); + fakeSampleStream.addFakeSampleStreamItem( + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + fakeSampleStream.addFakeSampleStreamItem( + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + fakeSampleStream.addFakeSampleStreamItem(FakeSampleStreamItem.END_OF_STREAM_ITEM); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + + int pos = 500; + do { + mediaCodecVideoRenderer.render(/* positionUs= */ pos, SystemClock.elapsedRealtime() * 1000); + pos += 250; + } while (!mediaCodecVideoRenderer.isEnded()); + + InOrder orderVerifier = inOrder(eventListener); + orderVerifier.verify(eventListener).onVideoSizeChanged(anyInt(), anyInt(), anyInt(), eq(1f)); + orderVerifier.verify(eventListener).onVideoSizeChanged(anyInt(), anyInt(), anyInt(), eq(2f)); + orderVerifier.verify(eventListener).onVideoSizeChanged(anyInt(), anyInt(), anyInt(), eq(3f)); + orderVerifier.verifyNoMoreInteractions(); + } + + @Test + public void render_includingResetPosition_keepsOutputFormatInVideoFrameMetadataListener() + throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + mediaCodecVideoRenderer.resetPosition(0); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + fakeSampleStream.addFakeSampleStreamItem( + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + fakeSampleStream.addFakeSampleStreamItem(FakeSampleStreamItem.END_OF_STREAM_ITEM); + int positionUs = 10; + do { + mediaCodecVideoRenderer.render(positionUs, SystemClock.elapsedRealtime() * 1000); + positionUs += 10; + } while (!mediaCodecVideoRenderer.isEnded()); + + assertThat(currentOutputFormat).isEqualTo(VIDEO_H264); + } + + @Test + public void enable_withMayRenderStartOfStream_rendersFirstFrameBeforeStart() throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + for (int i = 0; i < 10; i++) { + mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + } + + verify(eventListener).onRenderedFirstFrame(any()); + } + + @Test + public void enable_withoutMayRenderStartOfStream_doesNotRenderFirstFrameBeforeStart() + throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* offsetUs */ 0); + for (int i = 0; i < 10; i++) { + mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + } + + verify(eventListener, never()).onRenderedFirstFrame(any()); + } + + @Test + public void enable_withoutMayRenderStartOfStream_rendersFirstFrameAfterStart() throws Exception { + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME)); + + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* offsetUs */ 0); + mediaCodecVideoRenderer.start(); + for (int i = 0; i < 10; i++) { + mediaCodecVideoRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000); + } + + verify(eventListener).onRenderedFirstFrame(any()); + } + + @Test + public void replaceStream_whenStarted_rendersFirstFrameOfNewStream() throws Exception { + FakeSampleStream fakeSampleStream1 = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + mediaCodecVideoRenderer.start(); + + boolean replacedStream = false; + for (int i = 0; i <= 10; i++) { + mediaCodecVideoRenderer.render( + /* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); + if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) { + mediaCodecVideoRenderer.replaceStream( + new Format[] {VIDEO_H264}, fakeSampleStream2, /* offsetUs= */ 100); + replacedStream = true; + } + } + + verify(eventListener, times(2)).onRenderedFirstFrame(any()); + } + + @Test + public void replaceStream_whenNotStarted_doesNotRenderFirstFrameOfNewStream() throws Exception { + FakeSampleStream fakeSampleStream1 = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + /* format= */ VIDEO_H264, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(new byte[] {0}, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + + boolean replacedStream = false; + for (int i = 0; i < 10; i++) { + mediaCodecVideoRenderer.render( + /* positionUs= */ i * 10, SystemClock.elapsedRealtime() * 1000); + if (!replacedStream && mediaCodecVideoRenderer.hasReadStreamToEnd()) { + mediaCodecVideoRenderer.replaceStream( + new Format[] {VIDEO_H264}, fakeSampleStream2, /* offsetUs= */ 100); + replacedStream = true; + } + } + + verify(eventListener).onRenderedFirstFrame(any()); + + // Render to streamOffsetUs and verify the new first frame gets rendered. + mediaCodecVideoRenderer.render(/* positionUs= */ 100, SystemClock.elapsedRealtime() * 1000); + + verify(eventListener, times(2)).onRenderedFirstFrame(any()); + } + + @Test + public void onVideoFrameProcessingOffset_isCalledAfterOutputFormatChanges() + throws ExoPlaybackException { + Format mp4Uhd = VIDEO_H264.buildUpon().setWidth(3840).setHeight(2160).build(); + byte[] sampleData = new byte[0]; + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + /* format= */ mp4Uhd, + /* eventDispatcher= */ null, + /* firstSampleTimeUs= */ 0, + /* timeUsIncrement= */ 50, + new FakeSampleStreamItem(mp4Uhd), + new FakeSampleStreamItem(sampleData, C.BUFFER_FLAG_KEY_FRAME), + new FakeSampleStreamItem(VIDEO_H264), + new FakeSampleStreamItem(sampleData, C.BUFFER_FLAG_KEY_FRAME), + new FakeSampleStreamItem(sampleData, C.BUFFER_FLAG_KEY_FRAME), + new FakeSampleStreamItem(mp4Uhd), + new FakeSampleStreamItem(sampleData, C.BUFFER_FLAG_KEY_FRAME), + new FakeSampleStreamItem(sampleData, C.BUFFER_FLAG_KEY_FRAME), + new FakeSampleStreamItem(sampleData, C.BUFFER_FLAG_KEY_FRAME), + new FakeSampleStreamItem(VIDEO_H264), + new FakeSampleStreamItem(sampleData, C.BUFFER_FLAG_KEY_FRAME), + FakeSampleStreamItem.END_OF_STREAM_ITEM); + + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {mp4Uhd}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* offsetUs */ 0); + + mediaCodecVideoRenderer.setCurrentStreamFinal(); + mediaCodecVideoRenderer.start(); + + int positionUs = 10; + do { + mediaCodecVideoRenderer.render(positionUs, SystemClock.elapsedRealtime() * 1000); + positionUs += 10; + } while (!mediaCodecVideoRenderer.isEnded()); + mediaCodecVideoRenderer.stop(); + + InOrder orderVerifier = inOrder(eventListener); + orderVerifier.verify(eventListener).onVideoFrameProcessingOffset(anyLong(), eq(1), eq(mp4Uhd)); + orderVerifier + .verify(eventListener) + .onVideoFrameProcessingOffset(anyLong(), eq(2), eq(VIDEO_H264)); + orderVerifier.verify(eventListener).onVideoFrameProcessingOffset(anyLong(), eq(3), eq(mp4Uhd)); + orderVerifier + .verify(eventListener) + .onVideoFrameProcessingOffset(anyLong(), eq(1), eq(VIDEO_H264)); + orderVerifier.verifyNoMoreInteractions(); + } +}