mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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
|
@Override
|
||||||
public void onVolumeChanged(float audioVolume) {
|
public void onVolumeChanged(float audioVolume) {
|
||||||
EventTime eventTime = generateReadingMediaPeriodEventTime();
|
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.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;
|
||||||
|
|
@ -525,6 +526,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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
* #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<
|
||||||
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 {
|
||||||
.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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
/* 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 {
|
||||||
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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue