Add an event for the audio position advancing
Currently the audio renderer can become ready before the AudioTrack actually has enough data to play something, which means that the player may transition to the ready state before audio starts playing. This makes the player's current state transition not very useful for detecting when audio actually starts playing. This change adds a new event to notify apps when the audio position is increasing after a pause or seek/flush/reset event, and includes an estimate of the system time at which audio playout started. Issue: #7577 PiperOrigin-RevId: 327810040
This commit is contained in:
parent
6960dde8a8
commit
4b0e39e4b9
@ -2,6 +2,9 @@
|
||||
|
||||
### dev-v2 (not yet released)
|
||||
|
||||
* Audio: Add an event for the audio position starting to advance, to make it
|
||||
easier for apps to determine when audio playout started
|
||||
([#7577](https://github.com/google/ExoPlayer/issues/7577)).
|
||||
|
||||
### 2.12.0 (not yet released - targeted for 2020-08-TBD) ###
|
||||
|
||||
|
@ -2231,6 +2231,13 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioPositionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
|
@ -205,6 +205,14 @@ public class AnalyticsCollector
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
EventTime eventTime = generateReadingMediaPeriodEventTime();
|
||||
for (AnalyticsListener listener : listeners) {
|
||||
listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAudioUnderrun(
|
||||
int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
|
@ -479,6 +479,16 @@ public interface AnalyticsListener {
|
||||
*/
|
||||
default void onAudioInputFormatChanged(EventTime eventTime, Format format) {}
|
||||
|
||||
/**
|
||||
* Called when the audio position has increased for the first time since the last pause or
|
||||
* position reset.
|
||||
*
|
||||
* @param eventTime The event time.
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started.
|
||||
*/
|
||||
default void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {}
|
||||
|
||||
/**
|
||||
* Called when an audio underrun occurs.
|
||||
*
|
||||
|
@ -65,6 +65,15 @@ public interface AudioRendererEventListener {
|
||||
*/
|
||||
default void onAudioInputFormatChanged(Format format) {}
|
||||
|
||||
/**
|
||||
* Called when the audio position has increased for the first time since the last pause or
|
||||
* position reset.
|
||||
*
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started.
|
||||
*/
|
||||
default void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {}
|
||||
|
||||
/**
|
||||
* Called when an audio underrun occurs.
|
||||
*
|
||||
@ -89,7 +98,7 @@ public interface AudioRendererEventListener {
|
||||
*/
|
||||
default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {}
|
||||
|
||||
/** Dispatches events to a {@link AudioRendererEventListener}. */
|
||||
/** Dispatches events to an {@link AudioRendererEventListener}. */
|
||||
final class EventDispatcher {
|
||||
|
||||
@Nullable private final Handler handler;
|
||||
@ -106,20 +115,16 @@ public interface AudioRendererEventListener {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}.
|
||||
*/
|
||||
public void enabled(final DecoderCounters decoderCounters) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */
|
||||
public void enabled(DecoderCounters decoderCounters) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}.
|
||||
*/
|
||||
public void decoderInitialized(final String decoderName,
|
||||
final long initializedTimestampMs, final long initializationDurationMs) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}. */
|
||||
public void decoderInitialized(
|
||||
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() ->
|
||||
@ -129,18 +134,23 @@ public interface AudioRendererEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}.
|
||||
*/
|
||||
public void inputFormatChanged(final Format format) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */
|
||||
public void inputFormatChanged(Format format) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format));
|
||||
}
|
||||
}
|
||||
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioPositionAdvancing(long)}. */
|
||||
public void positionAdvancing(long playoutStartSystemTimeMs) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() -> castNonNull(listener).onAudioPositionAdvancing(playoutStartSystemTimeMs));
|
||||
}
|
||||
}
|
||||
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioUnderrun(int, long, long)}. */
|
||||
public void underrun(
|
||||
final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) {
|
||||
public void underrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() ->
|
||||
@ -149,10 +159,8 @@ public interface AudioRendererEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
|
||||
*/
|
||||
public void disabled(final DecoderCounters counters) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. */
|
||||
public void disabled(DecoderCounters counters) {
|
||||
counters.ensureUpdated();
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
@ -163,17 +171,15 @@ public interface AudioRendererEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.
|
||||
*/
|
||||
public void audioSessionId(final int audioSessionId) {
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */
|
||||
public void audioSessionId(int audioSessionId) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId));
|
||||
}
|
||||
}
|
||||
|
||||
/** Invokes {@link AudioRendererEventListener#onSkipSilenceEnabledChanged(boolean)}. */
|
||||
public void skipSilenceEnabledChanged(final boolean skipSilenceEnabled) {
|
||||
public void skipSilenceEnabledChanged(boolean skipSilenceEnabled) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onSkipSilenceEnabledChanged(skipSilenceEnabled));
|
||||
}
|
||||
|
@ -72,10 +72,19 @@ public interface AudioSink {
|
||||
*/
|
||||
void onPositionDiscontinuity();
|
||||
|
||||
/**
|
||||
* Called when the audio sink's position has increased for the first time since it was last
|
||||
* paused or flushed.
|
||||
*
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started. Only valid if the audio track has not underrun.
|
||||
*/
|
||||
default void onPositionAdvancing(long playoutStartSystemTimeMs) {}
|
||||
|
||||
/**
|
||||
* Called when the audio sink runs out of data.
|
||||
* <p>
|
||||
* An audio sink implementation may never call this method (for example, if audio data is
|
||||
*
|
||||
* <p>An audio sink implementation may never call this method (for example, if audio data is
|
||||
* consumed in batches rather than based on the sink's own clock).
|
||||
*
|
||||
* @param bufferSize The size of the sink's buffer, in bytes.
|
||||
|
@ -48,6 +48,15 @@ import java.lang.reflect.Method;
|
||||
/** Listener for position tracker events. */
|
||||
public interface Listener {
|
||||
|
||||
/**
|
||||
* Called when the position tracker's position has increased for the first time since it was
|
||||
* last paused or reset.
|
||||
*
|
||||
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
|
||||
* which playout started.
|
||||
*/
|
||||
void onPositionAdvancing(long playoutStartSystemTimeMs);
|
||||
|
||||
/**
|
||||
* Called when the frame position is too far from the expected frame position.
|
||||
*
|
||||
@ -145,6 +154,7 @@ import java.lang.reflect.Method;
|
||||
private boolean needsPassthroughWorkarounds;
|
||||
private long bufferSizeUs;
|
||||
private float audioTrackPlaybackSpeed;
|
||||
private boolean notifiedPositionIncreasing;
|
||||
|
||||
private long smoothedPlayheadOffsetUs;
|
||||
private long lastPlayheadSampleTimeUs;
|
||||
@ -287,9 +297,21 @@ import java.lang.reflect.Method;
|
||||
positionUs /= 1000;
|
||||
}
|
||||
|
||||
if (!notifiedPositionIncreasing && positionUs > lastPositionUs) {
|
||||
notifiedPositionIncreasing = true;
|
||||
long mediaDurationSinceLastPositionUs = C.usToMs(positionUs - lastPositionUs);
|
||||
long playoutDurationSinceLastPositionUs =
|
||||
Util.getPlayoutDurationForMediaDuration(
|
||||
mediaDurationSinceLastPositionUs, audioTrackPlaybackSpeed);
|
||||
long playoutStartSystemTimeMs =
|
||||
System.currentTimeMillis() - C.usToMs(playoutDurationSinceLastPositionUs);
|
||||
listener.onPositionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
|
||||
lastSystemTimeUs = systemTimeUs;
|
||||
lastPositionUs = positionUs;
|
||||
lastSampleUsedGetTimestampMode = useGetTimestampMode;
|
||||
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
@ -512,6 +534,7 @@ import java.lang.reflect.Method;
|
||||
lastPlayheadSampleTimeUs = 0;
|
||||
lastSystemTimeUs = 0;
|
||||
previousModeSystemTimeUs = 0;
|
||||
notifiedPositionIncreasing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -708,6 +708,11 @@ public abstract class DecoderAudioRenderer<
|
||||
DecoderAudioRenderer.this.onPositionDiscontinuity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
|
@ -1776,6 +1776,13 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
if (listener != null) {
|
||||
listener.onPositionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs) {
|
||||
if (listener != null) {
|
||||
|
@ -828,6 +828,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
MediaCodecAudioRenderer.this.onPositionDiscontinuity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
|
||||
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.underrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
|
@ -316,6 +316,12 @@ public class EventLogger implements AnalyticsListener {
|
||||
logd(eventTime, "audioInputFormat", Format.toLogString(format));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
|
||||
long timeSincePlayoutStartMs = System.currentTimeMillis() - playoutStartSystemTimeMs;
|
||||
logd(eventTime, "audioPositionAdvancing", "timeSincePlayoutStartMs=" + timeSincePlayoutStartMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioUnderrun(
|
||||
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
|
@ -106,21 +106,22 @@ public final class AnalyticsCollectorTest {
|
||||
private static final int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 26;
|
||||
private static final int EVENT_AUDIO_DISABLED = 27;
|
||||
private static final int EVENT_AUDIO_SESSION_ID = 28;
|
||||
private static final int EVENT_AUDIO_UNDERRUN = 29;
|
||||
private static final int EVENT_VIDEO_ENABLED = 30;
|
||||
private static final int EVENT_VIDEO_DECODER_INIT = 31;
|
||||
private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 32;
|
||||
private static final int EVENT_DROPPED_FRAMES = 33;
|
||||
private static final int EVENT_VIDEO_DISABLED = 34;
|
||||
private static final int EVENT_RENDERED_FIRST_FRAME = 35;
|
||||
private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 36;
|
||||
private static final int EVENT_VIDEO_SIZE_CHANGED = 37;
|
||||
private static final int EVENT_DRM_KEYS_LOADED = 38;
|
||||
private static final int EVENT_DRM_ERROR = 39;
|
||||
private static final int EVENT_DRM_KEYS_RESTORED = 40;
|
||||
private static final int EVENT_DRM_KEYS_REMOVED = 41;
|
||||
private static final int EVENT_DRM_SESSION_ACQUIRED = 42;
|
||||
private static final int EVENT_DRM_SESSION_RELEASED = 43;
|
||||
private static final int EVENT_AUDIO_POSITION_ADVANCING_ID = 29;
|
||||
private static final int EVENT_AUDIO_UNDERRUN = 30;
|
||||
private static final int EVENT_VIDEO_ENABLED = 31;
|
||||
private static final int EVENT_VIDEO_DECODER_INIT = 32;
|
||||
private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 33;
|
||||
private static final int EVENT_DROPPED_FRAMES = 34;
|
||||
private static final int EVENT_VIDEO_DISABLED = 35;
|
||||
private static final int EVENT_RENDERED_FIRST_FRAME = 36;
|
||||
private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 37;
|
||||
private static final int EVENT_VIDEO_SIZE_CHANGED = 38;
|
||||
private static final int EVENT_DRM_KEYS_LOADED = 39;
|
||||
private static final int EVENT_DRM_ERROR = 40;
|
||||
private static final int EVENT_DRM_KEYS_RESTORED = 41;
|
||||
private static final int EVENT_DRM_KEYS_REMOVED = 42;
|
||||
private static final int EVENT_DRM_SESSION_ACQUIRED = 43;
|
||||
private static final int EVENT_DRM_SESSION_RELEASED = 44;
|
||||
|
||||
private static final UUID DRM_SCHEME_UUID =
|
||||
UUID.nameUUIDFromBytes(TestUtil.createByteArray(7, 8, 9));
|
||||
@ -226,6 +227,7 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING_ID)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
|
||||
@ -305,6 +307,7 @@ public final class AnalyticsCollectorTest {
|
||||
.containsExactly(period0, period1)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING_ID)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT))
|
||||
.containsExactly(period0, period1)
|
||||
@ -380,6 +383,7 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING_ID)).containsExactly(period1);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0);
|
||||
@ -476,6 +480,9 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID))
|
||||
.containsExactly(period0, period1)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING_ID))
|
||||
.containsExactly(period0, period1)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0);
|
||||
@ -576,6 +583,9 @@ public final class AnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID))
|
||||
.containsExactly(period1Seq1, period1Seq2)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING_ID))
|
||||
.containsExactly(period1Seq1, period1Seq2)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0, period0);
|
||||
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT))
|
||||
@ -1922,6 +1932,11 @@ public final class AnalyticsCollectorTest {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_SESSION_ID, eventTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_POSITION_ADVANCING_ID, eventTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioUnderrun(
|
||||
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
|
@ -30,6 +30,7 @@ public class FakeAudioRenderer extends FakeRenderer {
|
||||
private final AudioRendererEventListener.EventDispatcher eventDispatcher;
|
||||
private final DecoderCounters decoderCounters;
|
||||
private boolean notifiedAudioSessionId;
|
||||
private boolean notifiedPositionAdvancing;
|
||||
|
||||
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
|
||||
super(C.TRACK_TYPE_AUDIO);
|
||||
@ -43,6 +44,7 @@ public class FakeAudioRenderer extends FakeRenderer {
|
||||
super.onEnabled(joining, mayRenderStartOfStream);
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
notifiedAudioSessionId = false;
|
||||
notifiedPositionAdvancing = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,6 +69,10 @@ public class FakeAudioRenderer extends FakeRenderer {
|
||||
eventDispatcher.audioSessionId(/* audioSessionId= */ 1);
|
||||
notifiedAudioSessionId = true;
|
||||
}
|
||||
if (shouldProcess && !notifiedPositionAdvancing) {
|
||||
eventDispatcher.positionAdvancing(System.currentTimeMillis());
|
||||
notifiedPositionAdvancing = true;
|
||||
}
|
||||
return shouldProcess;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user