From 55a13d8871ec982ab261c2d0078e1f406576915a Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 24 Sep 2020 16:32:49 +0100 Subject: [PATCH] Callback on audio track failure Intended for statistics now that all errors are not surfaced to the app. PiperOrigin-RevId: 333519898 --- .../analytics/AnalyticsCollector.java | 8 ++++ .../analytics/AnalyticsListener.java | 11 +++++ .../audio/AudioRendererEventListener.java | 32 ++++++++++++++ .../android/exoplayer2/audio/AudioSink.java | 24 +++++++++++ .../audio/DecoderAudioRenderer.java | 5 +++ .../exoplayer2/audio/DefaultAudioSink.java | 30 ++++++++----- .../audio/MediaCodecAudioRenderer.java | 5 +++ .../audio/MediaCodecAudioRendererTest.java | 42 ++++++++++++++++++- 8 files changed, 145 insertions(+), 12 deletions(-) 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 35f3099dc9..30321c5972 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 @@ -258,6 +258,14 @@ public class AnalyticsCollector } } + @Override + public void onAudioSinkError(Exception audioSinkError) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioSinkError(eventTime, audioSinkError); + } + } + @Override public void onVolumeChanged(float audioVolume) { EventTime eventTime = generateReadingMediaPeriodEventTime(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 2e26019541..7e5abbd803 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.LoadEventInfo; @@ -525,6 +526,16 @@ public interface AnalyticsListener { */ default void onSkipSilenceEnabledChanged(EventTime eventTime, boolean skipSilenceEnabled) {} + /** + * Called when {@link AudioSink} has encountered an error. These errors are just for informational + * purposes and the player may recover. + * + * @param eventTime The event time. + * @param audioSinkError Either a {@link AudioSink.InitializationException} or a {@link + * AudioSink.WriteException} describing the error. + */ + default void onAudioSinkError(EventTime eventTime, Exception audioSinkError) {} + /** * Called when the volume changes. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index f921141f24..e51948725b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -17,11 +17,14 @@ package com.google.android.exoplayer2.audio; import static com.google.android.exoplayer2.util.Util.castNonNull; +import android.media.AudioTrack; import android.os.Handler; import android.os.SystemClock; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.util.Assertions; @@ -98,6 +101,28 @@ public interface AudioRendererEventListener { */ default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {} + /** + * Called when {@link AudioSink} has encountered an error. + * + *

If the sink writes to a platform {@link AudioTrack}, this will called for all {@link + * AudioTrack} errors. + * + *

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, + * possibly with different settings) and continue. Hence applications should not + * 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. + * + *

Fatal errors that cannot be recovered will be reported wrapped in a {@link + * ExoPlaybackException} by {@link Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @param audioSinkError Either an {@link AudioSink.InitializationException} or a {@link + * AudioSink.WriteException} describing the error. + */ + default void onAudioSinkError(Exception audioSinkError) {} + /** Dispatches events to an {@link AudioRendererEventListener}. */ final class EventDispatcher { @@ -184,5 +209,12 @@ public interface AudioRendererEventListener { handler.post(() -> castNonNull(listener).onSkipSilenceEnabledChanged(skipSilenceEnabled)); } } + + /** Invokes {@link AudioRendererEventListener#onAudioSinkError(Exception)}. */ + public void audioSinkError(Exception audioSinkError) { + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioSinkError(audioSinkError)); + } + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 4f9e007d86..c3351ffbad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -19,8 +19,10 @@ import android.media.AudioTrack; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -113,6 +115,28 @@ public interface AudioSink { * #onOffloadBufferEmptying()} will be called. */ default void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {} + + /** + * Called when {@link AudioSink} has encountered an error. + * + *

If the sink writes to a platform {@link AudioTrack}, this will called for all {@link + * AudioTrack} errors. + * + *

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, + * possibly with different settings) and continue. Hence applications should not + * 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. + * + *

Fatal errors that cannot be recovered will be reported wrapped in a {@link + * ExoPlaybackException} by {@link Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @param audioSinkError Either an {@link AudioSink.InitializationException} or a {@link + * AudioSink.WriteException} describing the error. + */ + default void onAudioSinkError(Exception audioSinkError) {} } /** 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 0391fc95c9..c8f3d958d6 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 @@ -724,5 +724,10 @@ public abstract class DecoderAudioRenderer< public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { eventDispatcher.skipSilenceEnabledChanged(skipSilenceEnabled); } + + @Override + public void onAudioSinkError(Exception audioSinkError) { + eventDispatcher.audioSinkError(audioSinkError); + } } } 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 ee6dba839c..7701cd2b18 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 @@ -827,6 +827,9 @@ public final class DefaultAudioSink implements AudioSink { .buildAudioTrack(tunneling, audioAttributes, audioSessionId); } catch (InitializationException e) { maybeDisableOffload(); + if (listener != null) { + listener.onAudioSinkError(e); + } throw e; } } @@ -892,36 +895,43 @@ public final class DefaultAudioSink implements AudioSink { } } int bytesRemaining = buffer.remaining(); - int bytesWritten = 0; + int bytesWrittenOrError = 0; // Error if negative if (Util.SDK_INT < 21) { // outputMode == OUTPUT_MODE_PCM. // Work out how many bytes we can write without the risk of blocking. int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes); if (bytesToWrite > 0) { bytesToWrite = min(bytesRemaining, bytesToWrite); - bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); - if (bytesWritten > 0) { - preV21OutputBufferOffset += bytesWritten; - buffer.position(buffer.position() + bytesWritten); + bytesWrittenOrError = + audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWrittenOrError > 0) { // No error + preV21OutputBufferOffset += bytesWrittenOrError; + buffer.position(buffer.position() + bytesWrittenOrError); } } } else if (tunneling) { Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); - bytesWritten = + bytesWrittenOrError = writeNonBlockingWithAvSyncV21( audioTrack, buffer, bytesRemaining, avSyncPresentationTimeUs); } else { - bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + bytesWrittenOrError = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); - if (bytesWritten < 0) { - boolean isRecoverable = isAudioTrackDeadObject(bytesWritten); + if (bytesWrittenOrError < 0) { + int error = bytesWrittenOrError; + boolean isRecoverable = isAudioTrackDeadObject(error); if (isRecoverable) { maybeDisableOffload(); } - throw new WriteException(bytesWritten, isRecoverable); + WriteException e = new WriteException(error, isRecoverable); + if (listener != null) { + listener.onAudioSinkError(e); + } + throw e; } + int bytesWritten = bytesWrittenOrError; if (isOffloadedPlayback(audioTrack)) { // After calling AudioTrack.setOffloadEndOfStream, the AudioTrack internally stops and diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 75bc7d3b1a..e051aa1a3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -862,5 +862,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media wakeupListener.onSleep(bufferEmptyingDeadlineMs); } } + + @Override + public void onAudioSinkError(Exception audioSinkError) { + eventDispatcher.audioSinkError(audioSinkError); + } } } 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 index 922431d210..d3423485fb 100644 --- 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 @@ -22,10 +22,14 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; import android.media.MediaFormat; +import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; @@ -46,6 +50,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -71,6 +76,7 @@ public class MediaCodecAudioRendererTest { private MediaCodecSelector mediaCodecSelector; @Mock private AudioSink audioSink; + @Mock private AudioRendererEventListener audioRendererEventListener; @Before public void setUp() throws Exception { @@ -94,13 +100,15 @@ public class MediaCodecAudioRendererTest { /* forceDisableAdaptive= */ false, /* forceSecure= */ false)); + Handler eventHandler = new Handler(Looper.getMainLooper()); + mediaCodecAudioRenderer = new MediaCodecAudioRenderer( ApplicationProvider.getApplicationContext(), mediaCodecSelector, /* enableDecoderFallback= */ false, - /* eventHandler= */ null, - /* eventListener= */ null, + eventHandler, + audioRendererEventListener, audioSink); } @@ -279,6 +287,36 @@ public class MediaCodecAudioRendererTest { exceptionThrowingRenderer.render(/* positionUs= */ 750, SystemClock.elapsedRealtime() * 1000); } + @Test + public void + render_callsAudioRendererEventListener_whenAudioSinkListenerOnAudioSessionIdIsCalled() { + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(AudioSink.Listener.class); + verify(audioSink, atLeastOnce()).setListener(listenerCaptor.capture()); + AudioSink.Listener audioSinkListener = listenerCaptor.getValue(); + + int audioSessionId = 2; + audioSinkListener.onAudioSessionId(audioSessionId); + + shadowOf(Looper.getMainLooper()).idle(); + verify(audioRendererEventListener).onAudioSessionId(audioSessionId); + } + + @Test + public void + render_callsAudioRendererEventListener_whenAudioSinkListenerOnAudioSinkErrorIsCalled() { + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(AudioSink.Listener.class); + verify(audioSink, atLeastOnce()).setListener(listenerCaptor.capture()); + AudioSink.Listener audioSinkListener = listenerCaptor.getValue(); + + Exception error = new AudioSink.WriteException(/* errorCode= */ 1, /* isRecoverable= */ true); + audioSinkListener.onAudioSinkError(error); + + shadowOf(Looper.getMainLooper()).idle(); + verify(audioRendererEventListener).onAudioSinkError(error); + } + private static Format getAudioSinkFormat(Format inputFormat) { return new Format.Builder() .setSampleMimeType(MimeTypes.AUDIO_RAW)