Callback on audio track failure

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

PiperOrigin-RevId: 333519898
This commit is contained in:
krocard 2020-09-24 16:32:49 +01:00 committed by kim-vde
parent fad2846d1c
commit 55a13d8871
8 changed files with 145 additions and 12 deletions

View file

@ -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();

View file

@ -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.
*

View file

@ -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.
*
* <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}. */
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));
}
}
}
}

View file

@ -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.
*
* <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) {}
}
/**

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -862,5 +862,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
wakeupListener.onSleep(bufferEmptyingDeadlineMs);
}
}
@Override
public void onAudioSinkError(Exception audioSinkError) {
eventDispatcher.audioSinkError(audioSinkError);
}
}
}

View file

@ -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<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) {
return new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW)