Commit 55a13d88 by krocard Committed by kim-vde

Callback on audio track failure

Intended for statistics now that all errors
are not surfaced to the app.

PiperOrigin-RevId: 333519898
parent fad2846d
...@@ -259,6 +259,14 @@ public class AnalyticsCollector ...@@ -259,6 +259,14 @@ public class AnalyticsCollector
} }
@Override @Override
public void onAudioSinkError(Exception audioSinkError) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onAudioSinkError(eventTime, audioSinkError);
}
}
@Override
public void onVolumeChanged(float audioVolume) { public void onVolumeChanged(float audioVolume) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
......
...@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; ...@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline; 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.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
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;
...@@ -526,6 +527,16 @@ public interface AnalyticsListener { ...@@ -526,6 +527,16 @@ 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
* 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. * Called when the volume changes.
* *
* @param eventTime The event time. * @param eventTime The event time.
......
...@@ -17,11 +17,14 @@ package com.google.android.exoplayer2.audio; ...@@ -17,11 +17,14 @@ 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.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.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.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -98,6 +101,28 @@ public interface AudioRendererEventListener { ...@@ -98,6 +101,28 @@ public interface AudioRendererEventListener {
*/ */
default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {} default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {}
/**
* Called when {@link AudioSink} has encountered an error.
*
* <p>If the sink writes to a platform {@link AudioTrack}, this will called for all {@link
* AudioTrack} errors.
*
* <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,
* 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
* ({@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.
*
* <p>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}. */ /** Dispatches events to an {@link AudioRendererEventListener}. */
final class EventDispatcher { final class EventDispatcher {
...@@ -184,5 +209,12 @@ public interface AudioRendererEventListener { ...@@ -184,5 +209,12 @@ public interface AudioRendererEventListener {
handler.post(() -> castNonNull(listener).onSkipSilenceEnabledChanged(skipSilenceEnabled)); 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));
}
}
} }
} }
...@@ -19,8 +19,10 @@ import android.media.AudioTrack; ...@@ -19,8 +19,10 @@ import android.media.AudioTrack;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
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.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -113,6 +115,28 @@ public interface AudioSink { ...@@ -113,6 +115,28 @@ public interface AudioSink {
* #onOffloadBufferEmptying()} will be called. * #onOffloadBufferEmptying()} will be called.
*/ */
default void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {} default void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {}
/**
* Called when {@link AudioSink} has encountered an error.
*
* <p>If the sink writes to a platform {@link AudioTrack}, this will called for all {@link
* AudioTrack} errors.
*
* <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,
* 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
* ({@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.
*
* <p>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) {}
} }
/** /**
......
...@@ -724,5 +724,10 @@ public abstract class DecoderAudioRenderer< ...@@ -724,5 +724,10 @@ public abstract class DecoderAudioRenderer<
public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
eventDispatcher.skipSilenceEnabledChanged(skipSilenceEnabled); eventDispatcher.skipSilenceEnabledChanged(skipSilenceEnabled);
} }
@Override
public void onAudioSinkError(Exception audioSinkError) {
eventDispatcher.audioSinkError(audioSinkError);
}
} }
} }
...@@ -827,6 +827,9 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -827,6 +827,9 @@ public final class DefaultAudioSink implements AudioSink {
.buildAudioTrack(tunneling, audioAttributes, audioSessionId); .buildAudioTrack(tunneling, audioAttributes, audioSessionId);
} catch (InitializationException e) { } catch (InitializationException e) {
maybeDisableOffload(); maybeDisableOffload();
if (listener != null) {
listener.onAudioSinkError(e);
}
throw e; throw e;
} }
} }
...@@ -892,36 +895,43 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -892,36 +895,43 @@ public final class DefaultAudioSink implements AudioSink {
} }
} }
int bytesRemaining = buffer.remaining(); int bytesRemaining = buffer.remaining();
int bytesWritten = 0; int bytesWrittenOrError = 0; // Error if negative
if (Util.SDK_INT < 21) { // outputMode == OUTPUT_MODE_PCM. if (Util.SDK_INT < 21) { // outputMode == OUTPUT_MODE_PCM.
// Work out how many bytes we can write without the risk of blocking. // Work out how many bytes we can write without the risk of blocking.
int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes); int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes);
if (bytesToWrite > 0) { if (bytesToWrite > 0) {
bytesToWrite = min(bytesRemaining, bytesToWrite); bytesToWrite = min(bytesRemaining, bytesToWrite);
bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); bytesWrittenOrError =
if (bytesWritten > 0) { audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);
preV21OutputBufferOffset += bytesWritten; if (bytesWrittenOrError > 0) { // No error
buffer.position(buffer.position() + bytesWritten); preV21OutputBufferOffset += bytesWrittenOrError;
buffer.position(buffer.position() + bytesWrittenOrError);
} }
} }
} else if (tunneling) { } else if (tunneling) {
Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);
bytesWritten = bytesWrittenOrError =
writeNonBlockingWithAvSyncV21( writeNonBlockingWithAvSyncV21(
audioTrack, buffer, bytesRemaining, avSyncPresentationTimeUs); audioTrack, buffer, bytesRemaining, avSyncPresentationTimeUs);
} else { } else {
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); bytesWrittenOrError = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
} }
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
if (bytesWritten < 0) { if (bytesWrittenOrError < 0) {
boolean isRecoverable = isAudioTrackDeadObject(bytesWritten); int error = bytesWrittenOrError;
boolean isRecoverable = isAudioTrackDeadObject(error);
if (isRecoverable) { if (isRecoverable) {
maybeDisableOffload(); 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)) { if (isOffloadedPlayback(audioTrack)) {
// After calling AudioTrack.setOffloadEndOfStream, the AudioTrack internally stops and // After calling AudioTrack.setOffloadEndOfStream, the AudioTrack internally stops and
......
...@@ -862,5 +862,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -862,5 +862,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
wakeupListener.onSleep(bufferEmptyingDeadlineMs); wakeupListener.onSleep(bufferEmptyingDeadlineMs);
} }
} }
@Override
public void onAudioSinkError(Exception audioSinkError) {
eventDispatcher.audioSinkError(audioSinkError);
}
} }
} }
...@@ -22,10 +22,14 @@ import static org.junit.Assert.assertThrows; ...@@ -22,10 +22,14 @@ import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
...@@ -46,6 +50,7 @@ import org.junit.Before; ...@@ -46,6 +50,7 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoRule;
...@@ -71,6 +76,7 @@ public class MediaCodecAudioRendererTest { ...@@ -71,6 +76,7 @@ public class MediaCodecAudioRendererTest {
private MediaCodecSelector mediaCodecSelector; private MediaCodecSelector mediaCodecSelector;
@Mock private AudioSink audioSink; @Mock private AudioSink audioSink;
@Mock private AudioRendererEventListener audioRendererEventListener;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
...@@ -94,13 +100,15 @@ public class MediaCodecAudioRendererTest { ...@@ -94,13 +100,15 @@ public class MediaCodecAudioRendererTest {
/* forceDisableAdaptive= */ false, /* forceDisableAdaptive= */ false,
/* forceSecure= */ false)); /* forceSecure= */ false));
Handler eventHandler = new Handler(Looper.getMainLooper());
mediaCodecAudioRenderer = mediaCodecAudioRenderer =
new MediaCodecAudioRenderer( new MediaCodecAudioRenderer(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
mediaCodecSelector, mediaCodecSelector,
/* enableDecoderFallback= */ false, /* enableDecoderFallback= */ false,
/* eventHandler= */ null, eventHandler,
/* eventListener= */ null, audioRendererEventListener,
audioSink); audioSink);
} }
...@@ -279,6 +287,36 @@ public class MediaCodecAudioRendererTest { ...@@ -279,6 +287,36 @@ public class MediaCodecAudioRendererTest {
exceptionThrowingRenderer.render(/* positionUs= */ 750, SystemClock.elapsedRealtime() * 1000); exceptionThrowingRenderer.render(/* positionUs= */ 750, SystemClock.elapsedRealtime() * 1000);
} }
@Test
public void
render_callsAudioRendererEventListener_whenAudioSinkListenerOnAudioSessionIdIsCalled() {
final ArgumentCaptor<AudioSink.Listener> 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<AudioSink.Listener> 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) { private static Format getAudioSinkFormat(Format inputFormat) {
return new Format.Builder() return new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW) .setSampleMimeType(MimeTypes.AUDIO_RAW)
......
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