diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 60913dc485..f517a255ec 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 00c1e0bcc5..d927161d16 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 7c170742d7..0193c94deb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index f01d11ec25..d80ef5f70a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -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. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index c366f27f81..f921141f24 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -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)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index b0f76c0afb..21683bda48 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -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. - *

- * An audio sink implementation may never call this method (for example, if audio data is + * + *

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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index c1d8df5c75..540ee098ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -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; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java index bc8237c911..84a4e36d07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java @@ -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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 2da648b303..7075fce5d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 91c0f946ce..dfcc41d670 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 9c55248fea..4d1ebe0111 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -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) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 7d3dfdf103..5e483c9a22 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -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) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java index 5ed4e5bf5f..1e2f6159a5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java @@ -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; } }