Commit 8cc3cc4e by tonihei Committed by Oliver Woodman

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
parent e682f53b
...@@ -99,6 +99,8 @@ ...@@ -99,6 +99,8 @@
parameter ([#7582](https://github.com/google/ExoPlayer/issues/7582)). parameter ([#7582](https://github.com/google/ExoPlayer/issues/7582)).
* Distinguish between `offsetUs` and `startPositionUs` when passing new * Distinguish between `offsetUs` and `startPositionUs` when passing new
`SampleStreams` to `Renderers`. `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. * Video: Pass frame rate hint to `Surface.setFrameRate` on Android R devices.
* Track selection: * Track selection:
* Add `Player.getTrackSelector`. * Add `Player.getTrackSelector`.
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.CheckResult;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererCapabilities.FormatSupport;
...@@ -93,6 +94,12 @@ public final class ExoPlaybackException extends Exception { ...@@ -93,6 +94,12 @@ public final class ExoPlaybackException extends Exception {
/** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */
public final long timestampMs; 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; @Nullable private final Throwable cause;
/** /**
...@@ -192,7 +199,7 @@ public final class ExoPlaybackException extends Exception { ...@@ -192,7 +199,7 @@ public final class ExoPlaybackException extends Exception {
int rendererIndex, int rendererIndex,
@Nullable Format rendererFormat, @Nullable Format rendererFormat,
@FormatSupport int rendererFormatSupport) { @FormatSupport int rendererFormatSupport) {
super( this(
deriveMessage( deriveMessage(
type, type,
customMessage, customMessage,
...@@ -200,14 +207,35 @@ public final class ExoPlaybackException extends Exception { ...@@ -200,14 +207,35 @@ public final class ExoPlaybackException extends Exception {
rendererIndex, rendererIndex,
rendererFormat, rendererFormat,
rendererFormatSupport), 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.type = type;
this.cause = cause; this.cause = cause;
this.rendererName = rendererName; this.rendererName = rendererName;
this.rendererIndex = rendererIndex; this.rendererIndex = rendererIndex;
this.rendererFormat = rendererFormat; this.rendererFormat = rendererFormat;
this.rendererFormatSupport = rendererFormatSupport; this.rendererFormatSupport = rendererFormatSupport;
timestampMs = SystemClock.elapsedRealtime(); this.mediaPeriodId = mediaPeriodId;
this.timestampMs = timestampMs;
} }
/** /**
...@@ -250,6 +278,27 @@ public final class ExoPlaybackException extends Exception { ...@@ -250,6 +278,27 @@ public final class ExoPlaybackException extends Exception {
return (OutOfMemoryError) Assertions.checkNotNull(cause); 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 @Nullable
private static String deriveMessage( private static String deriveMessage(
@Type int type, @Type int type,
......
...@@ -528,6 +528,14 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -528,6 +528,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
maybeNotifyPlaybackInfoChanged(); maybeNotifyPlaybackInfoChanged();
} catch (ExoPlaybackException e) { } 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); Log.e(TAG, "Playback error", e);
stopInternal( stopInternal(
/* forceResetRenderers= */ true, /* forceResetRenderers= */ true,
...@@ -537,6 +545,11 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -537,6 +545,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
maybeNotifyPlaybackInfoChanged(); maybeNotifyPlaybackInfoChanged();
} catch (IOException e) { } catch (IOException e) {
ExoPlaybackException error = ExoPlaybackException.createForSource(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); Log.e(TAG, "Playback error", error);
stopInternal( stopInternal(
/* forceResetRenderers= */ false, /* forceResetRenderers= */ false,
......
...@@ -543,7 +543,10 @@ public class AnalyticsCollector ...@@ -543,7 +543,10 @@ public class AnalyticsCollector
@Override @Override
public final void onPlayerError(ExoPlaybackException error) { public final void onPlayerError(ExoPlaybackException error) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); EventTime eventTime =
error.mediaPeriodId != null
? generateEventTime(error.mediaPeriodId)
: generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error); listener.onPlayerError(eventTime, error);
} }
......
...@@ -53,6 +53,7 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; ...@@ -53,6 +53,7 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
import com.google.android.exoplayer2.testutil.FakeAudioRenderer; import com.google.android.exoplayer2.testutil.FakeAudioRenderer;
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm; import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.FakeMediaSource; 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;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.FakeVideoRenderer; import com.google.android.exoplayer2.testutil.FakeVideoRenderer;
...@@ -1450,6 +1451,111 @@ public final class AnalyticsCollectorTest { ...@@ -1450,6 +1451,111 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0); 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) { private void populateEventIds(Timeline timeline) {
period0 = period0 =
new EventWindowAndPeriodId( new EventWindowAndPeriodId(
...@@ -1508,6 +1614,14 @@ public final class AnalyticsCollectorTest { ...@@ -1508,6 +1614,14 @@ public final class AnalyticsCollectorTest {
new FakeVideoRenderer(eventHandler, videoRendererEventListener), new FakeVideoRenderer(eventHandler, videoRendererEventListener),
new FakeAudioRenderer(eventHandler, audioRendererEventListener) 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(); TestAnalyticsListener listener = new TestAnalyticsListener();
try { try {
new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext()) new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext())
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment