Emit onPositionDiscontinuity event when silence is skipped

Issue: androidx/media#765
PiperOrigin-RevId: 584024654
This commit is contained in:
tianyifeng 2023-11-20 07:47:43 -08:00 committed by Copybara-Service
parent 5eb6a889a7
commit 89bedf0fb5
12 changed files with 192 additions and 5 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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}.

View File

@ -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) {

View File

@ -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.

View File

@ -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.

View File

@ -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}. */

View File

@ -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);

View File

@ -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 {

View File

@ -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);

View File

@ -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 "?";
}

View File

@ -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() {