From 89bedf0fb5b9d8a07298a4331a968fc88dc4b999 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Mon, 20 Nov 2023 07:47:43 -0800 Subject: [PATCH] Emit onPositionDiscontinuity event when silence is skipped Issue: androidx/media#765 PiperOrigin-RevId: 584024654 --- RELEASENOTES.md | 2 + api.txt | 3 +- .../java/androidx/media3/common/Player.java | 6 +- .../media3/exoplayer/DefaultMediaClock.java | 7 ++ .../exoplayer/ExoPlayerImplInternal.java | 13 +++- .../androidx/media3/exoplayer/MediaClock.java | 5 ++ .../media3/exoplayer/audio/AudioSink.java | 3 + .../exoplayer/audio/DecoderAudioRenderer.java | 15 ++++ .../exoplayer/audio/DefaultAudioSink.java | 52 ++++++++++++- .../audio/MediaCodecAudioRenderer.java | 15 ++++ .../media3/exoplayer/util/EventLogger.java | 2 + .../media3/exoplayer/ExoPlayerTest.java | 74 +++++++++++++++++++ 12 files changed, 192 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d708c8f0fc..e816ccf51e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ `ImageRenderer.ImageOutput`. * `DefaultRenderersFactory` now provides an `ImageRenderer` to the player by default with null `ImageOutput` and `ImageDecoder.Factory.DEFAULT`. + * Emit `Player.Listener.onPositionDiscontinuity` event when silence is + skipped ([#765](https://github.com/androidx/media/issues/765)). * Transformer: * Add support for flattening H.265/HEVC SEF slow motion videos. * Increase transmuxing speed, especially for 'remove video' edits. diff --git a/api.txt b/api.txt index ee94bc21e6..ac4cc97233 100644 --- a/api.txt +++ b/api.txt @@ -826,6 +826,7 @@ package androidx.media3.common { field public static final int DISCONTINUITY_REASON_REMOVE = 4; // 0x4 field public static final int DISCONTINUITY_REASON_SEEK = 1; // 0x1 field public static final int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; // 0x2 + field public static final int DISCONTINUITY_REASON_SILENCE_SKIP = 6; // 0x6 field public static final int DISCONTINUITY_REASON_SKIP = 3; // 0x3 field public static final int EVENT_AUDIO_ATTRIBUTES_CHANGED = 20; // 0x14 field public static final int EVENT_AUDIO_SESSION_ID = 21; // 0x15 @@ -894,7 +895,7 @@ package androidx.media3.common { field public static final androidx.media3.common.Player.Commands EMPTY; } - @IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.DiscontinuityReason { + @IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL, androidx.media3.common.Player.DISCONTINUITY_REASON_SILENCE_SKIP}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.DiscontinuityReason { } @IntDef({androidx.media3.common.Player.EVENT_TIMELINE_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION, androidx.media3.common.Player.EVENT_TRACKS_CHANGED, androidx.media3.common.Player.EVENT_IS_LOADING_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED, androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED, androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED, androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_PLAYER_ERROR, androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY, androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_METADATA_CHANGED, androidx.media3.common.Player.EVENT_PLAYLIST_METADATA_CHANGED, androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, androidx.media3.common.Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_SESSION_ID, androidx.media3.common.Player.EVENT_VOLUME_CHANGED, androidx.media3.common.Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_SURFACE_SIZE_CHANGED, androidx.media3.common.Player.EVENT_VIDEO_SIZE_CHANGED, androidx.media3.common.Player.EVENT_RENDERED_FIRST_FRAME, androidx.media3.common.Player.EVENT_CUES, androidx.media3.common.Player.EVENT_METADATA, androidx.media3.common.Player.EVENT_DEVICE_INFO_CHANGED, androidx.media3.common.Player.EVENT_DEVICE_VOLUME_CHANGED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Event { diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index bab7d72a8b..c2964c8391 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -1395,7 +1395,8 @@ public interface Player { DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_SKIP, DISCONTINUITY_REASON_REMOVE, - DISCONTINUITY_REASON_INTERNAL + DISCONTINUITY_REASON_INTERNAL, + DISCONTINUITY_REASON_SILENCE_SKIP }) @interface DiscontinuityReason {} @@ -1427,6 +1428,9 @@ public interface Player { /** Discontinuity introduced internally (e.g. by the source). */ int DISCONTINUITY_REASON_INTERNAL = 5; + /** Discontinuity introduced by a skipped silence. */ + int DISCONTINUITY_REASON_SILENCE_SKIP = 6; + /** * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link * #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultMediaClock.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultMediaClock.java index 312b209c34..7b60f5835d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultMediaClock.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultMediaClock.java @@ -134,6 +134,13 @@ import androidx.media3.common.util.Clock; : Assertions.checkNotNull(rendererClock).getPositionUs(); } + @Override + public boolean hasSkippedSilenceSinceLastCall() { + return isUsingStandaloneClock + ? standaloneClock.hasSkippedSilenceSinceLastCall() + : Assertions.checkNotNull(rendererClock).hasSkippedSilenceSinceLastCall(); + } + @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (rendererClock != null) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 50bfd793fe..bac2804a7c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -993,7 +993,18 @@ import java.util.concurrent.atomic.AtomicBoolean; /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod()); long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs); - playbackInfo.updatePositionUs(periodPositionUs); + if (mediaClock.hasSkippedSilenceSinceLastCall()) { + playbackInfo = + handlePositionDiscontinuity( + playbackInfo.periodId, + /* positionUs= */ periodPositionUs, + playbackInfo.requestedContentPositionUs, + /* discontinuityStartPositionUs= */ periodPositionUs, + /* reportDiscontinuity= */ true, + Player.DISCONTINUITY_REASON_SILENCE_SKIP); + } else { + playbackInfo.updatePositionUs(periodPositionUs); + } } // Update the buffered position and total buffered duration. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaClock.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaClock.java index 4c351bccc3..38ac4331ce 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaClock.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaClock.java @@ -25,6 +25,11 @@ public interface MediaClock { /** Returns the current media position in microseconds. */ long getPositionUs(); + /** Returns whether there is a skipped silence since the last call to this method. */ + default boolean hasSkippedSilenceSinceLastCall() { + return false; + } + /** * Attempts to set the playback parameters. The media clock may override the speed if changing the * playback parameters is not supported. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java index 05eec98b78..ad6bd799fe 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java @@ -152,6 +152,9 @@ public interface AudioSink { * @param audioTrackConfig The {@link AudioTrackConfig} of the released {@link AudioTrack}. */ default void onAudioTrackReleased(AudioTrackConfig audioTrackConfig) {} + + /** Called when a period of silence has been skipped. */ + default void onSilenceSkipped() {} } /** Configuration parameters used for an {@link AudioTrack}. */ diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java index 03a8e941ae..1f263bee74 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java @@ -165,6 +165,7 @@ public abstract class DecoderAudioRenderer< private long outputStreamOffsetUs; private final long[] pendingOutputStreamOffsetsUs; private int pendingOutputStreamOffsetCount; + private boolean hasPendingReportedSkippedSilence; public DecoderAudioRenderer() { this(/* eventHandler= */ null, /* eventListener= */ null); @@ -577,6 +578,13 @@ public abstract class DecoderAudioRenderer< return currentPositionUs; } + @Override + public boolean hasSkippedSilenceSinceLastCall() { + boolean hasPendingReportedSkippedSilence = this.hasPendingReportedSkippedSilence; + this.hasPendingReportedSkippedSilence = false; + return hasPendingReportedSkippedSilence; + } + @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { audioSink.setPlaybackParameters(playbackParameters); @@ -606,6 +614,7 @@ public abstract class DecoderAudioRenderer< audioSink.flush(); currentPositionUs = positionUs; + hasPendingReportedSkippedSilence = false; allowPositionDiscontinuity = true; inputStreamEnded = false; outputStreamEnded = false; @@ -630,6 +639,7 @@ public abstract class DecoderAudioRenderer< inputFormat = null; audioTrackNeedsConfigure = true; setOutputStreamOffsetUs(C.TIME_UNSET); + hasPendingReportedSkippedSilence = false; try { setSourceDrmSession(null); releaseDecoder(); @@ -829,6 +839,11 @@ public abstract class DecoderAudioRenderer< DecoderAudioRenderer.this.onPositionDiscontinuity(); } + @Override + public void onSilenceSkipped() { + hasPendingReportedSkippedSilence = true; + } + @Override public void onPositionAdvancing(long playoutStartSystemTimeMs) { eventDispatcher.positionAdvancing(playoutStartSystemTimeMs); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 19166aec9c..199ca50fb7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -96,6 +96,16 @@ public final class DefaultAudioSink implements AudioSink { */ private static final int AUDIO_TRACK_SMALLER_BUFFER_RETRY_SIZE = 1_000_000; + /** The minimum duration of the skipped silence to be reported as discontinuity. */ + private static final int MINIMUM_REPORT_SKIPPED_SILENCE_DURATION_US = 1_000_000; + + /** + * The delay of reporting the skipped silence, during which the default audio sink checks if there + * is any further skipped silence that is close to the delayed silence. If any, the further + * skipped silence will be concatenated to the delayed one. + */ + private static final int REPORT_SKIPPED_SILENCE_DELAY_MS = 100; + /** * Thrown when the audio track has provided a spurious timestamp, if {@link * #failOnSpuriousAudioTimestamp} is set. @@ -542,6 +552,9 @@ public final class DefaultAudioSink implements AudioSink { private boolean offloadDisabledUntilNextConfiguration; private boolean isWaitingForOffloadEndOfStreamHandled; @Nullable private Looper playbackLooper; + private long skippedOutputFrameCountAtLastPosition; + private long accumulatedSkippedSilenceDurationUs; + private @MonotonicNonNull Handler reportSkippedSilenceHandler; @RequiresNonNull("#1.audioProcessorChain") private DefaultAudioSink(Builder builder) { @@ -1443,6 +1456,11 @@ public final class DefaultAudioSink implements AudioSink { } writeExceptionPendingExceptionHolder.clear(); initializationExceptionPendingExceptionHolder.clear(); + skippedOutputFrameCountAtLastPosition = 0; + accumulatedSkippedSilenceDurationUs = 0; + if (reportSkippedSilenceHandler != null) { + checkNotNull(reportSkippedSilenceHandler).removeCallbacksAndMessages(null); + } } @Override @@ -1645,8 +1663,28 @@ public final class DefaultAudioSink implements AudioSink { } private long applySkipping(long positionUs) { - return positionUs - + configuration.framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount()); + long skippedOutputFrameCountAtCurrentPosition = + audioProcessorChain.getSkippedOutputFrameCount(); + long adjustedPositionUs = + positionUs + configuration.framesToDurationUs(skippedOutputFrameCountAtCurrentPosition); + if (skippedOutputFrameCountAtCurrentPosition > skippedOutputFrameCountAtLastPosition) { + long silenceDurationUs = + configuration.framesToDurationUs( + skippedOutputFrameCountAtCurrentPosition - skippedOutputFrameCountAtLastPosition); + skippedOutputFrameCountAtLastPosition = skippedOutputFrameCountAtCurrentPosition; + handleSkippedSilence(silenceDurationUs); + } + return adjustedPositionUs; + } + + private void handleSkippedSilence(long silenceDurationUs) { + accumulatedSkippedSilenceDurationUs += silenceDurationUs; + if (reportSkippedSilenceHandler == null) { + reportSkippedSilenceHandler = new Handler(Looper.myLooper()); + } + reportSkippedSilenceHandler.removeCallbacksAndMessages(null); + reportSkippedSilenceHandler.postDelayed( + this::maybeReportSkippedSilence, /* delayMillis= */ REPORT_SKIPPED_SILENCE_DELAY_MS); } private boolean isAudioTrackInitialized() { @@ -2225,6 +2263,16 @@ public final class DefaultAudioSink implements AudioSink { } } + private void maybeReportSkippedSilence() { + if (accumulatedSkippedSilenceDurationUs >= MINIMUM_REPORT_SKIPPED_SILENCE_DURATION_US) { + // If the existing silence is already long enough, report the silence + listener.onSilenceSkipped(); + } + // Reset the accumulated silence anyway as the later silences are far from the current one + // and should be treated separately. + accumulatedSkippedSilenceDurationUs = 0; + } + @RequiresApi(23) private static final class AudioDeviceInfoApi23 { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index f2395eb1bc..5e7c854ea7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -118,6 +118,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean audioSinkNeedsReset; @Nullable private WakeupListener wakeupListener; + private boolean hasPendingReportedSkippedSilence; /** * @param context A context. @@ -613,6 +614,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media audioSink.flush(); currentPositionUs = positionUs; + hasPendingReportedSkippedSilence = false; allowPositionDiscontinuity = true; } @@ -646,6 +648,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onReset() { + hasPendingReportedSkippedSilence = false; try { super.onReset(); } finally { @@ -679,6 +682,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return currentPositionUs; } + @Override + public boolean hasSkippedSilenceSinceLastCall() { + boolean hasPendingReportedSkippedSilence = this.hasPendingReportedSkippedSilence; + this.hasPendingReportedSkippedSilence = false; + return hasPendingReportedSkippedSilence; + } + @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { audioSink.setPlaybackParameters(playbackParameters); @@ -969,6 +979,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media MediaCodecAudioRenderer.this.onPositionDiscontinuity(); } + @Override + public void onSilenceSkipped() { + hasPendingReportedSkippedSilence = true; + } + @Override public void onPositionAdvancing(long playoutStartSystemTimeMs) { eventDispatcher.positionAdvancing(playoutStartSystemTimeMs); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java index b056d17fc7..ef47c628f8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java @@ -707,6 +707,8 @@ public class EventLogger implements AnalyticsListener { return "SKIP"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; + case Player.DISCONTINUITY_REASON_SILENCE_SKIP: + return "SILENCE_SKIP"; default: return "?"; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 98548045e9..daa9b92239 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -14054,6 +14054,80 @@ public final class ExoPlayerTest { .isSameInstanceAs(mediaItem2); } + @Test + public void silenceSkipped_playerEmitOnPositionDiscontinuity() throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* isPlaceholder= */ false, + /* durationUs= */ 10 * C.MICROS_PER_SECOND, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ 0, + AdPlaybackState.NONE)); + FakeMediaClockRenderer audioRenderer = + new FakeMediaClockRenderer(C.TRACK_TYPE_AUDIO) { + private long offsetUs; + private long positionUs; + private boolean hasPendingReportedSkippedSilence; + + @Override + protected void onStreamChanged( + Format[] formats, long startPositionUs, long offsetUs, MediaPeriodId mediaPeriodId) { + this.offsetUs = offsetUs; + this.positionUs = offsetUs; + } + + @Override + public long getPositionUs() { + // Continuously increase position to let playback progress, and simulate the silence + // skip until it reaches some points of time. + if (positionUs - offsetUs == 10_000) { + hasPendingReportedSkippedSilence = true; + positionUs += 30_000; + } else { + positionUs += 10_000; + } + return positionUs; + } + + @Override + public boolean hasSkippedSilenceSinceLastCall() { + boolean hasPendingReportedSkippedSilence = this.hasPendingReportedSkippedSilence; + if (hasPendingReportedSkippedSilence) { + this.hasPendingReportedSkippedSilence = false; + } + return hasPendingReportedSkippedSilence; + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + }; + ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(audioRenderer).build(); + Player.Listener mockPlayerListener = mock(Player.Listener.class); + player.addListener(mockPlayerListener); + + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.prepare(); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + + verify(mockPlayerListener) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SILENCE_SKIP)); + assertThat(audioRenderer.isEnded).isTrue(); + + player.release(); + } + // Internal methods. private void addWatchAsSystemFeature() {