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