mirror of
https://github.com/samsonjs/media.git
synced 2026-04-04 11:05:47 +00:00
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:
parent
fad2846d1c
commit
55a13d8871
8 changed files with 145 additions and 12 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -862,5 +862,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
wakeupListener.onSleep(bufferEmptyingDeadlineMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioSinkError(Exception audioSinkError) {
|
||||
eventDispatcher.audioSinkError(audioSinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue