Add plumbing for reporting internal decoder errors

PiperOrigin-RevId: 357732695
This commit is contained in:
olly 2021-02-16 17:13:16 +00:00 committed by kim-vde
parent e009322edd
commit 1a34de5954
12 changed files with 188 additions and 26 deletions

View File

@ -9,6 +9,8 @@
* Audio: * Audio:
* Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases * Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases
([#8585](https://github.com/google/ExoPlayer/issues/8585)). ([#8585](https://github.com/google/ExoPlayer/issues/8585)).
* Analytics:
* Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`.
* Library restructuring: * Library restructuring:
* `DebugTextViewHelper` moved from `ui` package to `util` package. * `DebugTextViewHelper` moved from `ui` package to `util` package.
* Spherical UI components moved from `video.spherical` package to * Spherical UI components moved from `video.spherical` package to

View File

@ -2098,6 +2098,11 @@ public class SimpleExoPlayer extends BasePlayer
analyticsCollector.onVideoFrameProcessingOffset(totalProcessingOffsetUs, frameCount); analyticsCollector.onVideoFrameProcessingOffset(totalProcessingOffsetUs, frameCount);
} }
@Override
public void onVideoCodecError(Exception videoCodecError) {
analyticsCollector.onVideoCodecError(videoCodecError);
}
// AudioRendererEventListener implementation // AudioRendererEventListener implementation
@Override @Override
@ -2156,6 +2161,11 @@ public class SimpleExoPlayer extends BasePlayer
analyticsCollector.onAudioSinkError(audioSinkError); analyticsCollector.onAudioSinkError(audioSinkError);
} }
@Override
public void onAudioCodecError(Exception audioCodecError) {
analyticsCollector.onAudioCodecError(audioCodecError);
}
// TextOutput implementation // TextOutput implementation
@Override @Override

View File

@ -309,6 +309,15 @@ public class AnalyticsCollector
listener -> listener.onAudioSinkError(eventTime, audioSinkError)); listener -> listener.onAudioSinkError(eventTime, audioSinkError));
} }
@Override
public final void onAudioCodecError(Exception audioCodecError) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_AUDIO_CODEC_ERROR,
listener -> listener.onAudioCodecError(eventTime, audioCodecError));
}
// Additional audio events. // Additional audio events.
/** /**
@ -456,6 +465,15 @@ public class AnalyticsCollector
listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount)); listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount));
} }
@Override
public final void onVideoCodecError(Exception videoCodecError) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_VIDEO_CODEC_ERROR,
listener -> listener.onVideoCodecError(eventTime, videoCodecError));
}
// Additional video events. // Additional video events.
/** /**

View File

@ -17,6 +17,8 @@ package com.google.android.exoplayer2.analytics;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.os.Looper; import android.os.Looper;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.Surface; import android.view.Surface;
@ -35,6 +37,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderException;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.LoadEventInfo;
@ -198,6 +201,8 @@ public interface AnalyticsListener {
EVENT_DRM_KEYS_REMOVED, EVENT_DRM_KEYS_REMOVED,
EVENT_DRM_SESSION_RELEASED, EVENT_DRM_SESSION_RELEASED,
EVENT_PLAYER_RELEASED, EVENT_PLAYER_RELEASED,
EVENT_AUDIO_CODEC_ERROR,
EVENT_VIDEO_CODEC_ERROR,
}) })
@interface EventFlags {} @interface EventFlags {}
/** {@link Player#getCurrentTimeline()} changed. */ /** {@link Player#getCurrentTimeline()} changed. */
@ -312,6 +317,10 @@ public interface AnalyticsListener {
int EVENT_DRM_SESSION_RELEASED = 1035; int EVENT_DRM_SESSION_RELEASED = 1035;
/** The player was released. */ /** The player was released. */
int EVENT_PLAYER_RELEASED = 1036; int EVENT_PLAYER_RELEASED = 1036;
/** The audio codec encountered an error. */
int EVENT_AUDIO_CODEC_ERROR = 1037;
/** The video codec encountered an error. */
int EVENT_VIDEO_CODEC_ERROR = 1038;
/** Time information of an event. */ /** Time information of an event. */
final class EventTime { final class EventTime {
@ -649,8 +658,14 @@ public interface AnalyticsListener {
EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
/** /**
* Called when a media source loading error occurred. These errors are just for informational * Called when a media source loading error occurred.
* purposes and the player may recover. *
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* This method is called to provide the application with an opportunity to log the error if it
* wishes to do so.
* *
* @param eventTime The event time. * @param eventTime The event time.
* @param loadEventInfo The {@link LoadEventInfo} defining the load event. * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
@ -829,8 +844,14 @@ public interface AnalyticsListener {
default void onSkipSilenceEnabledChanged(EventTime eventTime, boolean skipSilenceEnabled) {} default void onSkipSilenceEnabledChanged(EventTime eventTime, boolean skipSilenceEnabled) {}
/** /**
* Called when {@link AudioSink} has encountered an error. These errors are just for informational * Called when {@link AudioSink} has encountered an error.
* purposes and the player may recover. *
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* This method is called to provide the application with an opportunity to log the error if it
* wishes to do so.
* *
* @param eventTime The event time. * @param eventTime The event time.
* @param audioSinkError Either a {@link AudioSink.InitializationException} or a {@link * @param audioSinkError Either a {@link AudioSink.InitializationException} or a {@link
@ -838,6 +859,22 @@ public interface AnalyticsListener {
*/ */
default void onAudioSinkError(EventTime eventTime, Exception audioSinkError) {} default void onAudioSinkError(EventTime eventTime, Exception audioSinkError) {}
/**
* Called when an audio decoder encounters an error.
*
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* This method is called to provide the application with an opportunity to log the error if it
* wishes to do so.
*
* @param eventTime The event time.
* @param audioCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/
default void onAudioCodecError(EventTime eventTime, Exception audioCodecError) {}
/** /**
* Called when the volume changes. * Called when the volume changes.
* *
@ -931,6 +968,22 @@ public interface AnalyticsListener {
default void onVideoFrameProcessingOffset( default void onVideoFrameProcessingOffset(
EventTime eventTime, long totalProcessingOffsetUs, int frameCount) {} EventTime eventTime, long totalProcessingOffsetUs, int frameCount) {}
/**
* Called when a video decoder encounters an error.
*
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* This method is called to provide the application with an opportunity to log the error if it
* wishes to do so.
*
* @param eventTime The event time.
* @param videoCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/
default void onVideoCodecError(EventTime eventTime, Exception videoCodecError) {}
/** /**
* Called when a frame is rendered for the first time since setting the surface, or since the * Called when a frame is rendered for the first time since setting the surface, or since the
* renderer was reset, or since the stream being rendered was changed. * renderer was reset, or since the stream being rendered was changed.
@ -987,8 +1040,14 @@ public interface AnalyticsListener {
default void onDrmKeysLoaded(EventTime eventTime) {} default void onDrmKeysLoaded(EventTime eventTime) {}
/** /**
* Called when a drm error occurs. These errors are just for informational purposes and the player * Called when a drm error occurs.
* may recover. *
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* This method is called to provide the application with an opportunity to log the error if it
* wishes to do so.
* *
* @param eventTime The event time. * @param eventTime The event time.
* @param error The error. * @param error The error.

View File

@ -18,15 +18,17 @@ package com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.media.AudioTrack; import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderException;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -113,6 +115,21 @@ public interface AudioRendererEventListener {
*/ */
default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {} default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {}
/**
* Called when an audio decoder encounters an error.
*
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* This method is called to provide the application with an opportunity to log the error if it
* wishes to do so.
*
* @param audioCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/
default void onAudioCodecError(Exception audioCodecError) {}
/** /**
* Called when {@link AudioSink} has encountered an error. * Called when {@link AudioSink} has encountered an error.
* *
@ -120,18 +137,14 @@ public interface AudioRendererEventListener {
* AudioTrack} errors. * AudioTrack} errors.
* *
* <p>This method being called does not indicate that playback has failed, or that it will fail. * <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error (for example by recreating the AudioTrack, * The player may be able to recover from the error. Hence applications should <em>not</em>
* possibly with different settings) and continue. Hence applications should <em>not</em> * implement this method to display a user visible error or initiate an application level retry.
* implement this method to display a user visible error or initiate an application level retry * {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* ({@link Player.EventListener#onPlayerError} is the appropriate place to implement such * This method is called to provide the application with an opportunity to log the error if it
* behavior). This method is called to provide the application with an opportunity to log the * wishes to do so.
* error if it wishes to do so.
* *
* <p>Fatal errors that cannot be recovered will be reported wrapped in a {@link * @param audioSinkError The error that occurred. Typically an {@link
* ExoPlaybackException} by {@link Player.EventListener#onPlayerError(ExoPlaybackException)}. * AudioSink.InitializationException} or a {@link AudioSink.WriteException}.
*
* @param audioSinkError Either an {@link AudioSink.InitializationException} or a {@link
* AudioSink.WriteException} describing the error.
*/ */
default void onAudioSinkError(Exception audioSinkError) {} default void onAudioSinkError(Exception audioSinkError) {}
@ -230,5 +243,12 @@ public interface AudioRendererEventListener {
handler.post(() -> castNonNull(listener).onAudioSinkError(audioSinkError)); handler.post(() -> castNonNull(listener).onAudioSinkError(audioSinkError));
} }
} }
/** Invokes {@link AudioRendererEventListener#onAudioCodecError(Exception)}. */
public void audioCodecError(Exception audioCodecError) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onAudioCodecError(audioCodecError));
}
}
} }
} }

View File

@ -300,6 +300,7 @@ public abstract class DecoderAudioRenderer<
while (feedInputBuffer()) {} while (feedInputBuffer()) {}
TraceUtil.endSection(); TraceUtil.endSection();
} catch (DecoderException e) { } catch (DecoderException e) {
eventDispatcher.audioCodecError(e);
throw createRendererException(e, inputFormat); throw createRendererException(e, inputFormat);
} catch (AudioSink.ConfigurationException e) { } catch (AudioSink.ConfigurationException e) {
throw createRendererException(e, e.format); throw createRendererException(e, e.format);
@ -618,7 +619,10 @@ public abstract class DecoderAudioRenderer<
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp); codecInitializedTimestamp - codecInitializingTimestamp);
decoderCounters.decoderInitCount++; decoderCounters.decoderInitCount++;
} catch (DecoderException | OutOfMemoryError e) { } catch (DecoderException e) {
eventDispatcher.audioCodecError(e);
throw createRendererException(e, inputFormat);
} catch (OutOfMemoryError e) {
throw createRendererException(e, inputFormat); throw createRendererException(e, inputFormat);
} }
} }

View File

@ -414,6 +414,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
eventDispatcher.decoderReleased(name); eventDispatcher.decoderReleased(name);
} }
@Override
protected void onCodecError(Exception codecError) {
eventDispatcher.audioCodecError(codecError);
}
@Override @Override
@Nullable @Nullable
protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder)

View File

@ -848,6 +848,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
decoderCounters.ensureUpdated(); decoderCounters.ensureUpdated();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
if (isMediaCodecException(e)) { if (isMediaCodecException(e)) {
onCodecError(e);
boolean isRecoverable = boolean isRecoverable =
enableRecoverableCodecExceptionRetries enableRecoverableCodecExceptionRetries
&& Util.SDK_INT >= 21 && Util.SDK_INT >= 21
@ -1406,6 +1407,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
// Do nothing. // Do nothing.
} }
/**
* Called when a codec error has occurred.
*
* <p>The default implementation is a no-op.
*
* @param codecError The error.
*/
protected void onCodecError(Exception codecError) {
// Do nothing.
}
/** /**
* Called when a new {@link Format} is read from the upstream {@link MediaPeriod}. * Called when a new {@link Format} is read from the upstream {@link MediaPeriod}.
* *

View File

@ -92,11 +92,11 @@ public interface MediaSourceEventListener {
* <em>not</em> be called in addition to this method. * <em>not</em> be called in addition to this method.
* *
* <p>This method being called does not indicate that playback has failed, or that it will fail. * <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error and continue. Hence applications should * The player may be able to recover from the error. Hence applications should <em>not</em>
* <em>not</em> implement this method to display a user visible error or initiate an application * implement this method to display a user visible error or initiate an application level retry.
* level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement * {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* such behavior). This method is called to provide the application with an opportunity to log the * This method is called to provide the application with an opportunity to log the error if it
* error if it wishes to do so. * wishes to do so.
* *
* @param windowIndex The window index in the timeline of the media source this load belongs to. * @param windowIndex The window index in the timeline of the media source this load belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not

View File

@ -206,6 +206,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
while (feedInputBuffer()) {} while (feedInputBuffer()) {}
TraceUtil.endSection(); TraceUtil.endSection();
} catch (DecoderException e) { } catch (DecoderException e) {
eventDispatcher.videoCodecError(e);
throw createRendererException(e, inputFormat); throw createRendererException(e, inputFormat);
} }
decoderCounters.ensureUpdated(); decoderCounters.ensureUpdated();
@ -707,7 +708,10 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
decoderInitializedTimestamp, decoderInitializedTimestamp,
decoderInitializedTimestamp - decoderInitializingTimestamp); decoderInitializedTimestamp - decoderInitializingTimestamp);
decoderCounters.decoderInitCount++; decoderCounters.decoderInitCount++;
} catch (DecoderException | OutOfMemoryError e) { } catch (DecoderException e) {
eventDispatcher.videoCodecError(e);
throw createRendererException(e, inputFormat);
} catch (OutOfMemoryError e) {
throw createRendererException(e, inputFormat); throw createRendererException(e, inputFormat);
} }
} }

View File

@ -690,6 +690,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
eventDispatcher.decoderReleased(name); eventDispatcher.decoderReleased(name);
} }
@Override
protected void onCodecError(Exception codecError) {
eventDispatcher.videoCodecError(codecError);
}
@Override @Override
@Nullable @Nullable
protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder)

View File

@ -17,14 +17,18 @@ package com.google.android.exoplayer2.video;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.Surface; import android.view.Surface;
import android.view.TextureView; import android.view.TextureView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderException;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -147,8 +151,21 @@ public interface VideoRendererEventListener {
default void onVideoDisabled(DecoderCounters counters) {} default void onVideoDisabled(DecoderCounters counters) {}
/** /**
* Dispatches events to a {@link VideoRendererEventListener}. * Called when a video decoder encounters an error.
*
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.EventListener#onPlayerError} is the appropriate place to implement such behavior.
* This method is called to provide the application with an opportunity to log the error if it
* wishes to do so.
*
* @param videoCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/ */
default void onVideoCodecError(Exception videoCodecError) {}
/** Dispatches events to a {@link VideoRendererEventListener}. */
final class EventDispatcher { final class EventDispatcher {
@Nullable private final Handler handler; @Nullable private final Handler handler;
@ -254,6 +271,12 @@ public interface VideoRendererEventListener {
} }
} }
/** Invokes {@link VideoRendererEventListener#onVideoCodecError(Exception)}. */
public void videoCodecError(Exception videoCodecError) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onVideoCodecError(videoCodecError));
}
}
} }
} }