From 8cc3cc4e148011cd3b7d05590996223023e926c9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 15 Jul 2020 18:00:57 +0100 Subject: [PATCH] Assume renderer errors are thrown for reading period. This fixes a bug that renderer errors are currently falsely associated with the playing period. PiperOrigin-RevId: 321381705 --- RELEASENOTES.md | 2 + .../exoplayer2/ExoPlaybackException.java | 55 ++- .../exoplayer2/ExoPlayerImplInternal.java | 13 + .../analytics/AnalyticsCollector.java | 5 +- .../android/exoplayer2/ExoPlayerTest.java | 383 ++++++++---------- .../analytics/AnalyticsCollectorTest.java | 114 ++++++ 6 files changed, 353 insertions(+), 219 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9d34211b1d..208997a1ae 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -99,6 +99,8 @@ parameter ([#7582](https://github.com/google/ExoPlayer/issues/7582)). * Distinguish between `offsetUs` and `startPositionUs` when passing new `SampleStreams` to `Renderers`. + * Fix wrong `MediaPeriodId` for some renderer errors reported by + `AnalyticsListener.onPlayerError`. * Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices. * Track selection: * Add `Player.getTrackSelector`. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index cd9662a251..31159c0e47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import android.os.SystemClock; import android.text.TextUtils; +import androidx.annotation.CheckResult; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; @@ -93,6 +94,12 @@ public final class ExoPlaybackException extends Exception { /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ public final long timestampMs; + /** + * The {@link MediaSource.MediaPeriodId} of the media associated with this error, or null if + * undetermined. + */ + @Nullable public final MediaSource.MediaPeriodId mediaPeriodId; + @Nullable private final Throwable cause; /** @@ -192,7 +199,7 @@ public final class ExoPlaybackException extends Exception { int rendererIndex, @Nullable Format rendererFormat, @FormatSupport int rendererFormatSupport) { - super( + this( deriveMessage( type, customMessage, @@ -200,14 +207,35 @@ public final class ExoPlaybackException extends Exception { rendererIndex, rendererFormat, rendererFormatSupport), - cause); + cause, + type, + rendererName, + rendererIndex, + rendererFormat, + rendererFormatSupport, + /* mediaPeriodId= */ null, + /* timestampMs= */ SystemClock.elapsedRealtime()); + } + + private ExoPlaybackException( + @Nullable String message, + @Nullable Throwable cause, + @Type int type, + @Nullable String rendererName, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + long timestampMs) { + super(message, cause); this.type = type; this.cause = cause; this.rendererName = rendererName; this.rendererIndex = rendererIndex; this.rendererFormat = rendererFormat; this.rendererFormatSupport = rendererFormatSupport; - timestampMs = SystemClock.elapsedRealtime(); + this.mediaPeriodId = mediaPeriodId; + this.timestampMs = timestampMs; } /** @@ -250,6 +278,27 @@ public final class ExoPlaybackException extends Exception { return (OutOfMemoryError) Assertions.checkNotNull(cause); } + /** + * Returns a copy of this exception with the provided {@link MediaSource.MediaPeriodId}. + * + * @param mediaPeriodId The {@link MediaSource.MediaPeriodId}. + * @return The copied exception. + */ + @CheckResult + /* package= */ ExoPlaybackException copyWithMediaPeriodId( + @Nullable MediaSource.MediaPeriodId mediaPeriodId) { + return new ExoPlaybackException( + getMessage(), + cause, + type, + rendererName, + rendererIndex, + rendererFormat, + rendererFormatSupport, + mediaPeriodId, + timestampMs); + } + @Nullable private static String deriveMessage( @Type int type, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 622b351e54..07f5234e24 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -528,6 +528,14 @@ import java.util.concurrent.atomic.AtomicBoolean; } maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { + if (e.type == ExoPlaybackException.TYPE_RENDERER) { + @Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod(); + if (readingPeriod != null) { + // We can assume that all renderer errors happen in the context of the reading period. See + // [internal: b/150584930#comment4] for exceptions that aren't covered by this assumption. + e = e.copyWithMediaPeriodId(readingPeriod.info.id); + } + } Log.e(TAG, "Playback error", e); stopInternal( /* forceResetRenderers= */ true, @@ -537,6 +545,11 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { ExoPlaybackException error = ExoPlaybackException.createForSource(e); + @Nullable MediaPeriodHolder playingPeriod = queue.getPlayingPeriod(); + if (playingPeriod != null) { + // We ensure that all IOException throwing methods are only executed for the playing period. + error = error.copyWithMediaPeriodId(playingPeriod.info.id); + } Log.e(TAG, "Playback error", error); stopInternal( /* forceResetRenderers= */ false, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 7fd8273c04..d7bda740b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -543,7 +543,10 @@ public class AnalyticsCollector @Override public final void onPlayerError(ExoPlaybackException error) { - EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + EventTime eventTime = + error.mediaPeriodId != null + ? generateEventTime(error.mediaPeriodId) + : generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 994ea7443f..4733637c82 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -3066,87 +3066,6 @@ public final class ExoPlayerTest { .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); } - @Test - public void secondMediaSourceInPlaylistOnlyThrowsWhenPreviousPeriodIsFullyRead() - throws Exception { - Timeline fakeTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = - new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); - MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, ExoPlayerTestRunner.VIDEO_FORMAT) { - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - throw new IOException(); - } - }; - ConcatenatingMediaSource concatenatingMediaSource = - new ConcatenatingMediaSource(workingMediaSource, failingMediaSource); - FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setMediaSources(concatenatingMediaSource) - .setRenderers(renderer) - .build(); - try { - testRunner.start().blockUntilEnded(TIMEOUT_MS); - fail(); - } catch (ExoPlaybackException e) { - // Expected exception. - } - assertThat(renderer.sampleBufferReadCount).isAtLeast(1); - assertThat(renderer.hasReadStreamToEnd()).isTrue(); - } - - @Test - public void - testDynamicallyAddedSecondMediaSourceInPlaylistOnlyThrowsWhenPreviousPeriodIsFullyRead() - throws Exception { - Timeline fakeTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = - new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT); - MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, ExoPlayerTestRunner.VIDEO_FORMAT) { - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - throw new IOException(); - } - }; - ConcatenatingMediaSource concatenatingMediaSource = - new ConcatenatingMediaSource(workingMediaSource); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - .executeRunnable(() -> concatenatingMediaSource.addMediaSource(failingMediaSource)) - .play() - .build(); - FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setMediaSources(concatenatingMediaSource) - .setActionSchedule(actionSchedule) - .setRenderers(renderer) - .build(); - try { - testRunner.start().blockUntilEnded(TIMEOUT_MS); - fail(); - } catch (ExoPlaybackException e) { - // Expected exception. - } - assertThat(renderer.sampleBufferReadCount).isAtLeast(1); - assertThat(renderer.hasReadStreamToEnd()).isTrue(); - } - @Test public void removingLoopingLastPeriodFromPlaylistDoesNotThrow() throws Exception { Timeline timeline = @@ -7172,140 +7091,6 @@ public final class ExoPlayerTest { assertArrayEquals(new int[] {1, 0}, currentWindowIndices); } - // TODO(b/150584930): Fix reporting of renderer errors. - @Ignore - @Test - public void errorThrownDuringRendererEnableAtPeriodTransition_isReportedForNewPeriod() { - FakeMediaSource source1 = - new FakeMediaSource( - new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT); - FakeMediaSource source2 = - new FakeMediaSource( - new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); - FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); - FakeRenderer audioRenderer = - new FakeRenderer(C.TRACK_TYPE_AUDIO) { - @Override - protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) - throws ExoPlaybackException { - // Fail when enabling the renderer. This will happen during the period transition. - throw createRendererException( - new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); - } - }; - AtomicReference trackGroupsAfterError = new AtomicReference<>(); - AtomicReference trackSelectionsAfterError = new AtomicReference<>(); - AtomicInteger windowIndexAfterError = new AtomicInteger(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - player.addAnalyticsListener( - new AnalyticsListener() { - @Override - public void onPlayerError( - EventTime eventTime, ExoPlaybackException error) { - trackGroupsAfterError.set(player.getCurrentTrackGroups()); - trackSelectionsAfterError.set(player.getCurrentTrackSelections()); - windowIndexAfterError.set(player.getCurrentWindowIndex()); - } - }); - } - }) - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setMediaSources(source1, source2) - .setActionSchedule(actionSchedule) - .setRenderers(videoRenderer, audioRenderer) - .build(); - - assertThrows( - ExoPlaybackException.class, - () -> - testRunner - .start(/* doPrepare= */ true) - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS)); - - assertThat(windowIndexAfterError.get()).isEqualTo(1); - assertThat(trackGroupsAfterError.get().length).isEqualTo(1); - assertThat(trackGroupsAfterError.get().get(0).getFormat(0)) - .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); - assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer. - assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer. - } - - // TODO(b/150584930): Fix reporting of renderer errors. - @Ignore - @Test - public void errorThrownDuringRendererReplaceStreamAtPeriodTransition_isReportedForNewPeriod() { - FakeMediaSource source1 = - new FakeMediaSource( - new FakeTimeline(/* windowCount= */ 1), - ExoPlayerTestRunner.VIDEO_FORMAT, - ExoPlayerTestRunner.AUDIO_FORMAT); - FakeMediaSource source2 = - new FakeMediaSource( - new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); - FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); - FakeRenderer audioRenderer = - new FakeRenderer(C.TRACK_TYPE_AUDIO) { - @Override - protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) - throws ExoPlaybackException { - // Fail when changing streams. This will happen during the period transition. - throw createRendererException( - new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); - } - }; - AtomicReference trackGroupsAfterError = new AtomicReference<>(); - AtomicReference trackSelectionsAfterError = new AtomicReference<>(); - AtomicInteger windowIndexAfterError = new AtomicInteger(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - player.addAnalyticsListener( - new AnalyticsListener() { - @Override - public void onPlayerError( - EventTime eventTime, ExoPlaybackException error) { - trackGroupsAfterError.set(player.getCurrentTrackGroups()); - trackSelectionsAfterError.set(player.getCurrentTrackSelections()); - windowIndexAfterError.set(player.getCurrentWindowIndex()); - } - }); - } - }) - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setMediaSources(source1, source2) - .setActionSchedule(actionSchedule) - .setRenderers(videoRenderer, audioRenderer) - .build(); - - assertThrows( - ExoPlaybackException.class, - () -> - testRunner - .start(/* doPrepare= */ true) - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS)); - - assertThat(windowIndexAfterError.get()).isEqualTo(1); - assertThat(trackGroupsAfterError.get().length).isEqualTo(1); - assertThat(trackGroupsAfterError.get().get(0).getFormat(0)) - .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); - assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer. - assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer. - } - @Test public void errorThrownDuringPlaylistUpdate_keepsConsistentPlayerState() { FakeMediaSource source1 = @@ -8218,6 +8003,174 @@ public final class ExoPlayerTest { exoPlayerTestRunner.assertMediaItemsTransitionedSame(initialMediaItem); } + @Test + public void + mediaSourceMaybeThrowSourceInfoRefreshError_isNotThrownUntilPlaybackReachedFailingItem() + throws Exception { + ExoPlayer player = new TestExoPlayer.Builder(context).build(); + player.addMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1))); + player.addMediaSource( + new FakeMediaSource(/* timeline= */ null) { + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + throw new IOException(); + } + }); + + player.prepare(); + player.play(); + ExoPlaybackException error = TestExoPlayer.runUntilError(player); + + Object period1Uid = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true) + .uid; + assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + } + + @Test + public void mediaPeriodMaybeThrowPrepareError_isNotThrownUntilPlaybackReachedFailingItem() + throws Exception { + ExoPlayer player = new TestExoPlayer.Builder(context).build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + player.addMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); + player.addMediaSource( + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + @Nullable TransferListener transferListener) { + return new FakeMediaPeriod( + trackGroupArray, + /* singleSampleTimeUs= */ 0, + mediaSourceEventDispatcher, + DrmSessionManager.DUMMY, + drmEventDispatcher, + /* deferOnPrepared= */ true) { + @Override + public void maybeThrowPrepareError() throws IOException { + throw new IOException(); + } + }; + } + }); + + player.prepare(); + player.play(); + ExoPlaybackException error = TestExoPlayer.runUntilError(player); + + Object period1Uid = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true) + .uid; + assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + } + + @Test + public void sampleStreamMaybeThrowError_isNotThrownUntilPlaybackReachedFailingItem() + throws Exception { + ExoPlayer player = new TestExoPlayer.Builder(context).build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + player.addMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); + player.addMediaSource( + new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + @Nullable TransferListener transferListener) { + return new FakeMediaPeriod( + trackGroupArray, /* singleSampleTimeUs= */ 0, mediaSourceEventDispatcher) { + @Override + protected SampleStream createSampleStream( + long positionUs, + TrackSelection selection, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher) { + return new FakeSampleStream( + mediaSourceEventDispatcher, + DrmSessionManager.DUMMY, + drmEventDispatcher, + selection.getSelectedFormat(), + /* fakeSampleStreamItems= */ ImmutableList.of()) { + @Override + public void maybeThrowError() throws IOException { + throw new IOException(); + } + }; + } + }; + } + }); + + player.prepare(); + player.play(); + ExoPlaybackException error = TestExoPlayer.runUntilError(player); + + Object period1Uid = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true) + .uid; + assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + } + + @Test + public void rendererError_isReportedWithReadingMediaPeriodId() throws Exception { + FakeMediaSource source0 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT); + FakeMediaSource source1 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + RenderersFactory renderersFactory = + (eventHandler, videoListener, audioListener, textOutput, metadataOutput) -> + new Renderer[] { + new FakeRenderer(C.TRACK_TYPE_VIDEO), + new FakeRenderer(C.TRACK_TYPE_AUDIO) { + @Override + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + // Fail when enabling the renderer. This will happen during the period + // transition while the reading and playing period are different. + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); + } + } + }; + ExoPlayer player = + new TestExoPlayer.Builder(context).setRenderersFactory(renderersFactory).build(); + player.setMediaSources(ImmutableList.of(source0, source1)); + player.prepare(); + player.play(); + + ExoPlaybackException error = TestExoPlayer.runUntilError(player); + + Object period1Uid = + player + .getCurrentTimeline() + .getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true) + .uid; + assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid); + // Verify test setup by checking that playing period was indeed different. + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index b462d8617b..c94e39dd68 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -53,6 +53,7 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.FakeAudioRenderer; import com.google.android.exoplayer2.testutil.FakeExoMediaDrm; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeVideoRenderer; @@ -1450,6 +1451,111 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0); } + @Test + public void onPlayerError_thrownDuringRendererEnableAtPeriodTransition_isReportedForNewPeriod() + throws Exception { + FakeMediaSource source0 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT); + FakeMediaSource source1 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + RenderersFactory renderersFactory = + (eventHandler, videoListener, audioListener, textOutput, metadataOutput) -> + new Renderer[] { + new FakeRenderer(C.TRACK_TYPE_VIDEO), + new FakeRenderer(C.TRACK_TYPE_AUDIO) { + @Override + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + // Fail when enabling the renderer. This will happen during the period transition. + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); + } + } + }; + + TestAnalyticsListener listener = + runAnalyticsTest( + new ConcatenatingMediaSource(source0, source1), + /* actionSchedule= */ null, + renderersFactory); + + populateEventIds(listener.lastReportedTimeline); + assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1); + } + + @Test + public void onPlayerError_thrownDuringRenderAtPeriodTransition_isReportedForNewPeriod() + throws Exception { + FakeMediaSource source0 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT); + FakeMediaSource source1 = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + RenderersFactory renderersFactory = + (eventHandler, videoListener, audioListener, textOutput, metadataOutput) -> + new Renderer[] { + new FakeRenderer(C.TRACK_TYPE_VIDEO), + new FakeRenderer(C.TRACK_TYPE_AUDIO) { + @Override + public void render(long positionUs, long realtimeUs) throws ExoPlaybackException { + // Fail when rendering the audio stream. This will happen during the period + // transition. + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); + } + } + }; + + TestAnalyticsListener listener = + runAnalyticsTest( + new ConcatenatingMediaSource(source0, source1), + /* actionSchedule= */ null, + renderersFactory); + + populateEventIds(listener.lastReportedTimeline); + assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1); + } + + @Test + public void + onPlayerError_thrownDuringRendererReplaceStreamAtPeriodTransition_isReportedForNewPeriod() + throws Exception { + FakeMediaSource source = + new FakeMediaSource( + new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.AUDIO_FORMAT); + RenderersFactory renderersFactory = + (eventHandler, videoListener, audioListener, textOutput, metadataOutput) -> + new Renderer[] { + new FakeRenderer(C.TRACK_TYPE_AUDIO) { + private int streamChangeCount = 0; + + @Override + protected void onStreamChanged( + Format[] formats, long startPositionUs, long offsetUs) + throws ExoPlaybackException { + // Fail when changing streams for the second time. This will happen during the + // period transition (as the first time is when enabling the stream initially). + if (++streamChangeCount == 2) { + throw createRendererException( + new IllegalStateException(), ExoPlayerTestRunner.AUDIO_FORMAT); + } + } + } + }; + + TestAnalyticsListener listener = + runAnalyticsTest( + new ConcatenatingMediaSource(source, source), + /* actionSchedule= */ null, + renderersFactory); + + populateEventIds(listener.lastReportedTimeline); + assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1); + } + private void populateEventIds(Timeline timeline) { period0 = new EventWindowAndPeriodId( @@ -1508,6 +1614,14 @@ public final class AnalyticsCollectorTest { new FakeVideoRenderer(eventHandler, videoRendererEventListener), new FakeAudioRenderer(eventHandler, audioRendererEventListener) }; + return runAnalyticsTest(mediaSource, actionSchedule, renderersFactory); + } + + private static TestAnalyticsListener runAnalyticsTest( + MediaSource mediaSource, + @Nullable ActionSchedule actionSchedule, + RenderersFactory renderersFactory) + throws Exception { TestAnalyticsListener listener = new TestAnalyticsListener(); try { new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext())