Skip to content

Commit

Permalink
Emit onPositionDiscontinuity event when silence is skipped
Browse files Browse the repository at this point in the history
Issue: #765
PiperOrigin-RevId: 584024654
  • Loading branch information
tianyif authored and copybara-github committed Nov 20, 2023
1 parent 5eb6a88 commit 89bedf0
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 5 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,8 @@ default void onMetadata(Metadata metadata) {}
DISCONTINUITY_REASON_SEEK_ADJUSTMENT,
DISCONTINUITY_REASON_SKIP,
DISCONTINUITY_REASON_REMOVE,
DISCONTINUITY_REASON_INTERNAL
DISCONTINUITY_REASON_INTERNAL,
DISCONTINUITY_REASON_SILENCE_SKIP
})
@interface DiscontinuityReason {}

Expand Down Expand Up @@ -1427,6 +1428,9 @@ default void onMetadata(Metadata metadata) {}
/** 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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ public long getPositionUs() {
: 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,18 @@ private void updatePlaybackPositions() throws ExoPlaybackException {
/* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ default void onAudioTrackInitialized(AudioTrackConfig audioTrackConfig) {}
* @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}. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -577,6 +578,13 @@ public long getPositionUs() {
return currentPositionUs;
}

@Override
public boolean hasSkippedSilenceSinceLastCall() {
boolean hasPendingReportedSkippedSilence = this.hasPendingReportedSkippedSilence;
this.hasPendingReportedSkippedSilence = false;
return hasPendingReportedSkippedSilence;
}

@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
audioSink.setPlaybackParameters(playbackParameters);
Expand Down Expand Up @@ -606,6 +614,7 @@ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlayb
audioSink.flush();

currentPositionUs = positionUs;
hasPendingReportedSkippedSilence = false;
allowPositionDiscontinuity = true;
inputStreamEnded = false;
outputStreamEnded = false;
Expand All @@ -630,6 +639,7 @@ protected void onDisabled() {
inputFormat = null;
audioTrackNeedsConfigure = true;
setOutputStreamOffsetUs(C.TIME_UNSET);
hasPendingReportedSkippedSilence = false;
try {
setSourceDrmSession(null);
releaseDecoder();
Expand Down Expand Up @@ -829,6 +839,11 @@ public void onPositionDiscontinuity() {
DecoderAudioRenderer.this.onPositionDiscontinuity();
}

@Override
public void onSilenceSkipped() {
hasPendingReportedSkippedSilence = true;
}

@Override
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -542,6 +552,9 @@ public DefaultAudioSink build() {
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) {
Expand Down Expand Up @@ -1443,6 +1456,11 @@ public void flush() {
}
writeExceptionPendingExceptionHolder.clear();
initializationExceptionPendingExceptionHolder.clear();
skippedOutputFrameCountAtLastPosition = 0;
accumulatedSkippedSilenceDurationUs = 0;
if (reportSkippedSilenceHandler != null) {
checkNotNull(reportSkippedSilenceHandler).removeCallbacksAndMessages(null);
}
}

@Override
Expand Down Expand Up @@ -1645,8 +1663,28 @@ private long applyMediaPositionParameters(long positionUs) {
}

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() {
Expand Down Expand Up @@ -2225,6 +2263,16 @@ public void clear() {
}
}

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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -613,6 +614,7 @@ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlayb
audioSink.flush();

currentPositionUs = positionUs;
hasPendingReportedSkippedSilence = false;
allowPositionDiscontinuity = true;
}

Expand Down Expand Up @@ -646,6 +648,7 @@ protected void onDisabled() {

@Override
protected void onReset() {
hasPendingReportedSkippedSilence = false;
try {
super.onReset();
} finally {
Expand Down Expand Up @@ -679,6 +682,13 @@ public long getPositionUs() {
return currentPositionUs;
}

@Override
public boolean hasSkippedSilenceSinceLastCall() {
boolean hasPendingReportedSkippedSilence = this.hasPendingReportedSkippedSilence;
this.hasPendingReportedSkippedSilence = false;
return hasPendingReportedSkippedSilence;
}

@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
audioSink.setPlaybackParameters(playbackParameters);
Expand Down Expand Up @@ -969,6 +979,11 @@ public void onPositionDiscontinuity() {
MediaCodecAudioRenderer.this.onPositionDiscontinuity();
}

@Override
public void onSilenceSkipped() {
hasPendingReportedSkippedSilence = true;
}

@Override
public void onPositionAdvancing(long playoutStartSystemTimeMs) {
eventDispatcher.positionAdvancing(playoutStartSystemTimeMs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,8 @@ private static String getDiscontinuityReasonString(@Player.DiscontinuityReason i
return "SKIP";
case Player.DISCONTINUITY_REASON_INTERNAL:
return "INTERNAL";
case Player.DISCONTINUITY_REASON_SILENCE_SKIP:
return "SILENCE_SKIP";
default:
return "?";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14054,6 +14054,80 @@ protected void onStreamChanged(
.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() {
Expand Down

0 comments on commit 89bedf0

Please sign in to comment.