mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add scrubbing mode API to ExoPlayer
This initial version prevents incoming seeks from pre-empting in-progress seeks, allowing more seeks to complete when the scrubber bar is dragged quickly back and forth. More scrubbing optimizations will be added in follow-up changes. PiperOrigin-RevId: 743052527
This commit is contained in:
parent
c8a34ec846
commit
d315d90f7a
@ -12,6 +12,9 @@
|
|||||||
* Fix issue where media item transition fails due to recoverable renderer
|
* Fix issue where media item transition fails due to recoverable renderer
|
||||||
error during initialization of the next media item
|
error during initialization of the next media item
|
||||||
([#2229](https://github.com/androidx/media/issues/2229)).
|
([#2229](https://github.com/androidx/media/issues/2229)).
|
||||||
|
* Add `ExoPlayer.setScrubbingModeEnabled(boolean)` method. This optimizes
|
||||||
|
the player for many frequent seeks (for example, from a user dragging a
|
||||||
|
scrubber bar around).
|
||||||
* Transformer:
|
* Transformer:
|
||||||
* Filling an initial gap (added via `addGap()`) with silent audio now
|
* Filling an initial gap (added via `addGap()`) with silent audio now
|
||||||
requires explicitly setting `setForceAudioTrack(true)` in
|
requires explicitly setting `setForceAudioTrack(true)` in
|
||||||
|
3
api.txt
3
api.txt
@ -885,6 +885,7 @@ package androidx.media3.common {
|
|||||||
field public static final int MEDIA_ITEM_TRANSITION_REASON_REPEAT = 0; // 0x0
|
field public static final int MEDIA_ITEM_TRANSITION_REASON_REPEAT = 0; // 0x0
|
||||||
field public static final int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2; // 0x2
|
field public static final int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2; // 0x2
|
||||||
field public static final int PLAYBACK_SUPPRESSION_REASON_NONE = 0; // 0x0
|
field public static final int PLAYBACK_SUPPRESSION_REASON_NONE = 0; // 0x0
|
||||||
|
field public static final int PLAYBACK_SUPPRESSION_REASON_SCRUBBING = 4; // 0x4
|
||||||
field public static final int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; // 0x1
|
field public static final int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; // 0x1
|
||||||
field public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT = 3; // 0x3
|
field public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT = 3; // 0x3
|
||||||
field @Deprecated public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2; // 0x2
|
field @Deprecated public static final int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE = 2; // 0x2
|
||||||
@ -969,7 +970,7 @@ package androidx.media3.common {
|
|||||||
@IntDef({androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG}) @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.PlayWhenReadyChangeReason {
|
@IntDef({androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG}) @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.PlayWhenReadyChangeReason {
|
||||||
}
|
}
|
||||||
|
|
||||||
@IntDef({androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}) @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.PlaybackSuppressionReason {
|
@IntDef({androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING}) @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.PlaybackSuppressionReason {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Player.PositionInfo {
|
public static final class Player.PositionInfo {
|
||||||
|
@ -1284,11 +1284,17 @@ public interface Player {
|
|||||||
int PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG = 6;
|
int PLAY_WHEN_READY_CHANGE_REASON_SUPPRESSED_TOO_LONG = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One
|
* Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}.
|
||||||
* of {@link #PLAYBACK_SUPPRESSION_REASON_NONE}, {@link
|
*
|
||||||
* #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}, {@link
|
* <p>One of:
|
||||||
* #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE} or {@link
|
*
|
||||||
* #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}.
|
* <ul>
|
||||||
|
* <li>{@link #PLAYBACK_SUPPRESSION_REASON_NONE}
|
||||||
|
* <li>{@link #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}
|
||||||
|
* <li>{@link #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE}
|
||||||
|
* <li>{@link #PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT}
|
||||||
|
* <li>{@link #PLAYBACK_SUPPRESSION_REASON_SCRUBBING}
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
||||||
// with Kotlin usages from before TYPE_USE was added.
|
// with Kotlin usages from before TYPE_USE was added.
|
||||||
@ -1300,7 +1306,8 @@ public interface Player {
|
|||||||
PLAYBACK_SUPPRESSION_REASON_NONE,
|
PLAYBACK_SUPPRESSION_REASON_NONE,
|
||||||
PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS,
|
PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS,
|
||||||
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE,
|
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_ROUTE,
|
||||||
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT
|
PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT,
|
||||||
|
PLAYBACK_SUPPRESSION_REASON_SCRUBBING
|
||||||
})
|
})
|
||||||
@interface PlaybackSuppressionReason {}
|
@interface PlaybackSuppressionReason {}
|
||||||
|
|
||||||
@ -1321,6 +1328,9 @@ public interface Player {
|
|||||||
*/
|
*/
|
||||||
int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT = 3;
|
int PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT = 3;
|
||||||
|
|
||||||
|
/** Playback is suppressed because the player is currently scrubbing. */
|
||||||
|
int PLAYBACK_SUPPRESSION_REASON_SCRUBBING = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
|
* Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
|
||||||
* #REPEAT_MODE_ALL}.
|
* #REPEAT_MODE_ALL}.
|
||||||
|
@ -1449,6 +1449,20 @@ public interface ExoPlayer extends Player {
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
boolean getSkipSilenceEnabled();
|
boolean getSkipSilenceEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether to optimize the player for scrubbing (many frequent seeks).
|
||||||
|
*
|
||||||
|
* <p>The player may consume more resources in this mode, so it should only be used for short
|
||||||
|
* periods of time in response to user interaction (e.g. dragging on a progress bar UI element).
|
||||||
|
*
|
||||||
|
* <p>During scrubbing mode playback is {@linkplain Player#getPlaybackSuppressionReason()
|
||||||
|
* suppressed} with {@link Player#PLAYBACK_SUPPRESSION_REASON_SCRUBBING}.
|
||||||
|
*
|
||||||
|
* @param scrubbingModeEnabled Whether scrubbing mode should be enabled.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
void setScrubbingModeEnabled(boolean scrubbingModeEnabled);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a {@link List} of {@linkplain Effect video effects} that will be applied to each video
|
* Sets a {@link List} of {@linkplain Effect video effects} that will be applied to each video
|
||||||
* frame.
|
* frame.
|
||||||
|
@ -35,7 +35,6 @@ import static androidx.media3.exoplayer.Renderer.MSG_SET_PRIORITY;
|
|||||||
import static androidx.media3.exoplayer.Renderer.MSG_SET_SCALING_MODE;
|
import static androidx.media3.exoplayer.Renderer.MSG_SET_SCALING_MODE;
|
||||||
import static androidx.media3.exoplayer.Renderer.MSG_SET_SKIP_SILENCE_ENABLED;
|
import static androidx.media3.exoplayer.Renderer.MSG_SET_SKIP_SILENCE_ENABLED;
|
||||||
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_EFFECTS;
|
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_EFFECTS;
|
||||||
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER;
|
|
||||||
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_OUTPUT_RESOLUTION;
|
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_OUTPUT_RESOLUTION;
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
@ -182,6 +181,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
private @DiscontinuityReason int pendingDiscontinuityReason;
|
private @DiscontinuityReason int pendingDiscontinuityReason;
|
||||||
private boolean pendingDiscontinuity;
|
private boolean pendingDiscontinuity;
|
||||||
private boolean foregroundMode;
|
private boolean foregroundMode;
|
||||||
|
private boolean scrubbingModeEnabled;
|
||||||
private SeekParameters seekParameters;
|
private SeekParameters seekParameters;
|
||||||
private ShuffleOrder shuffleOrder;
|
private ShuffleOrder shuffleOrder;
|
||||||
private PreloadConfiguration preloadConfiguration;
|
private PreloadConfiguration preloadConfiguration;
|
||||||
@ -370,7 +370,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
playbackInfoUpdateListener,
|
playbackInfoUpdateListener,
|
||||||
playerId,
|
playerId,
|
||||||
builder.playbackLooperProvider,
|
builder.playbackLooperProvider,
|
||||||
preloadConfiguration);
|
preloadConfiguration,
|
||||||
|
frameMetadataListener);
|
||||||
Looper playbackLooper = internalPlayer.getPlaybackLooper();
|
Looper playbackLooper = internalPlayer.getPlaybackLooper();
|
||||||
|
|
||||||
volume = 1;
|
volume = 1;
|
||||||
@ -447,8 +448,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
sendRendererMessage(
|
sendRendererMessage(
|
||||||
TRACK_TYPE_VIDEO, MSG_SET_CHANGE_FRAME_RATE_STRATEGY, videoChangeFrameRateStrategy);
|
TRACK_TYPE_VIDEO, MSG_SET_CHANGE_FRAME_RATE_STRATEGY, videoChangeFrameRateStrategy);
|
||||||
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_SKIP_SILENCE_ENABLED, skipSilenceEnabled);
|
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_SKIP_SILENCE_ENABLED, skipSilenceEnabled);
|
||||||
sendRendererMessage(
|
|
||||||
TRACK_TYPE_VIDEO, MSG_SET_VIDEO_FRAME_METADATA_LISTENER, frameMetadataListener);
|
|
||||||
sendRendererMessage(
|
sendRendererMessage(
|
||||||
TRACK_TYPE_CAMERA_MOTION, MSG_SET_CAMERA_MOTION_LISTENER, frameMetadataListener);
|
TRACK_TYPE_CAMERA_MOTION, MSG_SET_CAMERA_MOTION_LISTENER, frameMetadataListener);
|
||||||
sendRendererMessage(MSG_SET_PRIORITY, priority);
|
sendRendererMessage(MSG_SET_PRIORITY, priority);
|
||||||
@ -1553,6 +1552,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
listener -> listener.onSkipSilenceEnabledChanged(newSkipSilenceEnabled));
|
listener -> listener.onSkipSilenceEnabledChanged(newSkipSilenceEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScrubbingModeEnabled(boolean scrubbingModeEnabled) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
if (scrubbingModeEnabled == this.scrubbingModeEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scrubbingModeEnabled = scrubbingModeEnabled;
|
||||||
|
internalPlayer.setScrubbingModeEnabled(scrubbingModeEnabled);
|
||||||
|
updatePlayWhenReady(playbackInfo.playWhenReady, playbackInfo.playWhenReadyChangeReason);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AnalyticsCollector getAnalyticsCollector() {
|
public AnalyticsCollector getAnalyticsCollector() {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
@ -2761,6 +2771,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private @PlaybackSuppressionReason int computePlaybackSuppressionReason(boolean playWhenReady) {
|
private @PlaybackSuppressionReason int computePlaybackSuppressionReason(boolean playWhenReady) {
|
||||||
|
if (scrubbingModeEnabled) {
|
||||||
|
return Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING;
|
||||||
|
}
|
||||||
if (suitableOutputChecker != null
|
if (suitableOutputChecker != null
|
||||||
&& !suitableOutputChecker.isSelectedOutputSuitableForPlayback()) {
|
&& !suitableOutputChecker.isSelectedOutputSuitableForPlayback()) {
|
||||||
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
|
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
|
||||||
|
@ -28,6 +28,7 @@ import static java.lang.Math.max;
|
|||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.media.MediaFormat;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
@ -72,6 +73,7 @@ import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
|||||||
import androidx.media3.exoplayer.trackselection.TrackSelector;
|
import androidx.media3.exoplayer.trackselection.TrackSelector;
|
||||||
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
|
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
|
||||||
import androidx.media3.exoplayer.upstream.BandwidthMeter;
|
import androidx.media3.exoplayer.upstream.BandwidthMeter;
|
||||||
|
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||||
import com.google.common.base.Supplier;
|
import com.google.common.base.Supplier;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -89,7 +91,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
MediaSourceList.MediaSourceListInfoRefreshListener,
|
MediaSourceList.MediaSourceListInfoRefreshListener,
|
||||||
PlaybackParametersListener,
|
PlaybackParametersListener,
|
||||||
PlayerMessage.Sender,
|
PlayerMessage.Sender,
|
||||||
AudioFocusManager.PlayerControl {
|
AudioFocusManager.PlayerControl,
|
||||||
|
VideoFrameMetadataListener {
|
||||||
|
|
||||||
private static final String TAG = "ExoPlayerImplInternal";
|
private static final String TAG = "ExoPlayerImplInternal";
|
||||||
|
|
||||||
@ -168,6 +171,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
private static final int MSG_SET_VOLUME = 32;
|
private static final int MSG_SET_VOLUME = 32;
|
||||||
private static final int MSG_AUDIO_FOCUS_PLAYER_COMMAND = 33;
|
private static final int MSG_AUDIO_FOCUS_PLAYER_COMMAND = 33;
|
||||||
private static final int MSG_AUDIO_FOCUS_VOLUME_MULTIPLIER = 34;
|
private static final int MSG_AUDIO_FOCUS_VOLUME_MULTIPLIER = 34;
|
||||||
|
private static final int MSG_SET_VIDEO_FRAME_METADATA_LISTENER = 35;
|
||||||
|
private static final int MSG_SET_SCRUBBING_MODE_ENABLED = 36;
|
||||||
|
private static final int MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE = 37;
|
||||||
|
|
||||||
private static final long BUFFERING_MAXIMUM_INTERVAL_MS =
|
private static final long BUFFERING_MAXIMUM_INTERVAL_MS =
|
||||||
Util.usToMs(Renderer.DEFAULT_DURATION_TO_PROGRESS_US);
|
Util.usToMs(Renderer.DEFAULT_DURATION_TO_PROGRESS_US);
|
||||||
@ -216,6 +222,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
private final boolean hasSecondaryRenderers;
|
private final boolean hasSecondaryRenderers;
|
||||||
private final AudioFocusManager audioFocusManager;
|
private final AudioFocusManager audioFocusManager;
|
||||||
private SeekParameters seekParameters;
|
private SeekParameters seekParameters;
|
||||||
|
private boolean scrubbingModeEnabled;
|
||||||
|
private boolean seekIsPendingWhileScrubbing;
|
||||||
|
@Nullable private SeekPosition queuedSeekWhileScrubbing;
|
||||||
private PlaybackInfo playbackInfo;
|
private PlaybackInfo playbackInfo;
|
||||||
private PlaybackInfoUpdate playbackInfoUpdate;
|
private PlaybackInfoUpdate playbackInfoUpdate;
|
||||||
private boolean released;
|
private boolean released;
|
||||||
@ -265,7 +274,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
PlaybackInfoUpdateListener playbackInfoUpdateListener,
|
PlaybackInfoUpdateListener playbackInfoUpdateListener,
|
||||||
PlayerId playerId,
|
PlayerId playerId,
|
||||||
@Nullable PlaybackLooperProvider playbackLooperProvider,
|
@Nullable PlaybackLooperProvider playbackLooperProvider,
|
||||||
PreloadConfiguration preloadConfiguration) {
|
PreloadConfiguration preloadConfiguration,
|
||||||
|
VideoFrameMetadataListener videoFrameMetadataListener) {
|
||||||
this.playbackInfoUpdateListener = playbackInfoUpdateListener;
|
this.playbackInfoUpdateListener = playbackInfoUpdateListener;
|
||||||
this.trackSelector = trackSelector;
|
this.trackSelector = trackSelector;
|
||||||
this.emptyTrackSelectorResult = emptyTrackSelectorResult;
|
this.emptyTrackSelectorResult = emptyTrackSelectorResult;
|
||||||
@ -340,6 +350,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
handler = clock.createHandler(this.playbackLooper, this);
|
handler = clock.createHandler(this.playbackLooper, this);
|
||||||
|
|
||||||
audioFocusManager = new AudioFocusManager(context, playbackLooper, /* playerControl= */ this);
|
audioFocusManager = new AudioFocusManager(context, playbackLooper, /* playerControl= */ this);
|
||||||
|
VideoFrameMetadataListener internalVideoFrameMetadataListener =
|
||||||
|
(presentationTimeUs, releaseTimeNs, format, mediaFormat) -> {
|
||||||
|
videoFrameMetadataListener.onVideoFrameAboutToBeRendered(
|
||||||
|
presentationTimeUs, releaseTimeNs, format, mediaFormat);
|
||||||
|
onVideoFrameAboutToBeRendered(presentationTimeUs, releaseTimeNs, format, mediaFormat);
|
||||||
|
};
|
||||||
|
handler
|
||||||
|
.obtainMessage(MSG_SET_VIDEO_FRAME_METADATA_LISTENER, internalVideoFrameMetadataListener)
|
||||||
|
.sendToTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaPeriodHolder createMediaPeriodHolder(
|
private MediaPeriodHolder createMediaPeriodHolder(
|
||||||
@ -405,6 +424,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget();
|
handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setScrubbingModeEnabled(boolean scrubbingModeEnabled) {
|
||||||
|
handler.obtainMessage(MSG_SET_SCRUBBING_MODE_ENABLED, scrubbingModeEnabled).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
handler.obtainMessage(MSG_STOP).sendToTarget();
|
handler.obtainMessage(MSG_STOP).sendToTarget();
|
||||||
}
|
}
|
||||||
@ -483,6 +506,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
setVolumeInternal(volume);
|
setVolumeInternal(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setVideoFrameMetadataListenerInternal(
|
||||||
|
VideoFrameMetadataListener videoFrameMetadataListener) throws ExoPlaybackException {
|
||||||
|
for (RendererHolder renderer : renderers) {
|
||||||
|
renderer.setVideoFrameMetadataListener(videoFrameMetadataListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void sendMessage(PlayerMessage message) {
|
public synchronized void sendMessage(PlayerMessage message) {
|
||||||
if (released || !playbackLooper.getThread().isAlive()) {
|
if (released || !playbackLooper.getThread().isAlive()) {
|
||||||
@ -613,6 +643,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
handler.obtainMessage(MSG_AUDIO_FOCUS_PLAYER_COMMAND, playerCommand, 0).sendToTarget();
|
handler.obtainMessage(MSG_AUDIO_FOCUS_PLAYER_COMMAND, playerCommand, 0).sendToTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VideoFrameMetadataListener implementation
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVideoFrameAboutToBeRendered(
|
||||||
|
long presentationTimeUs,
|
||||||
|
long releaseTimeNs,
|
||||||
|
Format format,
|
||||||
|
@Nullable MediaFormat mediaFormat) {
|
||||||
|
if (seekIsPendingWhileScrubbing) {
|
||||||
|
handler.obtainMessage(MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE).sendToTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handler.Callback implementation.
|
// Handler.Callback implementation.
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "WrongConstant"}) // Casting message payload types and IntDef.
|
@SuppressWarnings({"unchecked", "WrongConstant"}) // Casting message payload types and IntDef.
|
||||||
@ -643,7 +686,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
doSomeWork();
|
doSomeWork();
|
||||||
break;
|
break;
|
||||||
case MSG_SEEK_TO:
|
case MSG_SEEK_TO:
|
||||||
seekToInternal((SeekPosition) msg.obj);
|
seekToInternal((SeekPosition) msg.obj, /* incrementAcks= */ true);
|
||||||
|
break;
|
||||||
|
case MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE:
|
||||||
|
seekIsPendingWhileScrubbing = false;
|
||||||
|
if (queuedSeekWhileScrubbing != null) {
|
||||||
|
seekToInternal(queuedSeekWhileScrubbing, /* incrementAcks= */ false);
|
||||||
|
queuedSeekWhileScrubbing = null;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case MSG_SET_PLAYBACK_PARAMETERS:
|
case MSG_SET_PLAYBACK_PARAMETERS:
|
||||||
setPlaybackParametersInternal((PlaybackParameters) msg.obj);
|
setPlaybackParametersInternal((PlaybackParameters) msg.obj);
|
||||||
@ -651,6 +701,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
case MSG_SET_SEEK_PARAMETERS:
|
case MSG_SET_SEEK_PARAMETERS:
|
||||||
setSeekParametersInternal((SeekParameters) msg.obj);
|
setSeekParametersInternal((SeekParameters) msg.obj);
|
||||||
break;
|
break;
|
||||||
|
case MSG_SET_SCRUBBING_MODE_ENABLED:
|
||||||
|
setScrubbingModeEnabledInternal((Boolean) msg.obj);
|
||||||
|
break;
|
||||||
case MSG_SET_FOREGROUND_MODE:
|
case MSG_SET_FOREGROUND_MODE:
|
||||||
setForegroundModeInternal(
|
setForegroundModeInternal(
|
||||||
/* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj);
|
/* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj);
|
||||||
@ -725,6 +778,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
case MSG_AUDIO_FOCUS_VOLUME_MULTIPLIER:
|
case MSG_AUDIO_FOCUS_VOLUME_MULTIPLIER:
|
||||||
handleAudioFocusVolumeMultiplierChange();
|
handleAudioFocusVolumeMultiplierChange();
|
||||||
break;
|
break;
|
||||||
|
case MSG_SET_VIDEO_FRAME_METADATA_LISTENER:
|
||||||
|
setVideoFrameMetadataListenerInternal((VideoFrameMetadataListener) msg.obj);
|
||||||
|
break;
|
||||||
case MSG_RELEASE:
|
case MSG_RELEASE:
|
||||||
releaseInternal();
|
releaseInternal();
|
||||||
// Return immediately to not send playback info updates after release.
|
// Return immediately to not send playback info updates after release.
|
||||||
@ -1486,8 +1542,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
MSG_DO_SOME_WORK, thisOperationStartTimeMs + wakeUpTimeIntervalMs);
|
MSG_DO_SOME_WORK, thisOperationStartTimeMs + wakeUpTimeIntervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
|
private void seekToInternal(SeekPosition seekPosition, boolean incrementAcks)
|
||||||
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
throws ExoPlaybackException {
|
||||||
|
playbackInfoUpdate.incrementPendingOperationAcks(incrementAcks ? 1 : 0);
|
||||||
|
if (seekIsPendingWhileScrubbing) {
|
||||||
|
queuedSeekWhileScrubbing = seekPosition;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MediaPeriodId periodId;
|
MediaPeriodId periodId;
|
||||||
long periodPositionUs;
|
long periodPositionUs;
|
||||||
@ -1568,6 +1629,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
seekIsPendingWhileScrubbing = scrubbingModeEnabled;
|
||||||
newPeriodPositionUs =
|
newPeriodPositionUs =
|
||||||
seekToPeriodPosition(
|
seekToPeriodPosition(
|
||||||
periodId,
|
periodId,
|
||||||
@ -1698,6 +1760,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
this.seekParameters = seekParameters;
|
this.seekParameters = seekParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setScrubbingModeEnabledInternal(boolean scrubbingModeEnabled)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
this.scrubbingModeEnabled = scrubbingModeEnabled;
|
||||||
|
if (!scrubbingModeEnabled) {
|
||||||
|
seekIsPendingWhileScrubbing = false;
|
||||||
|
handler.removeMessages(MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE);
|
||||||
|
if (queuedSeekWhileScrubbing != null) {
|
||||||
|
// Immediately seek to the latest received scrub position (interrupting a pending seek).
|
||||||
|
seekToInternal(queuedSeekWhileScrubbing, /* incrementAcks= */ false);
|
||||||
|
queuedSeekWhileScrubbing = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setForegroundModeInternal(
|
private void setForegroundModeInternal(
|
||||||
boolean foregroundMode, @Nullable AtomicBoolean processedFlag) {
|
boolean foregroundMode, @Nullable AtomicBoolean processedFlag) {
|
||||||
if (this.foregroundMode != foregroundMode) {
|
if (this.foregroundMode != foregroundMode) {
|
||||||
@ -1773,6 +1849,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
boolean releaseMediaSourceList,
|
boolean releaseMediaSourceList,
|
||||||
boolean resetError) {
|
boolean resetError) {
|
||||||
handler.removeMessages(MSG_DO_SOME_WORK);
|
handler.removeMessages(MSG_DO_SOME_WORK);
|
||||||
|
seekIsPendingWhileScrubbing = false;
|
||||||
|
queuedSeekWhileScrubbing = null;
|
||||||
pendingRecoverableRendererError = null;
|
pendingRecoverableRendererError = null;
|
||||||
updateRebufferingState(/* isRebuffering= */ false, /* resetLastRebufferRealtimeMs= */ true);
|
updateRebufferingState(/* isRebuffering= */ false, /* resetLastRebufferRealtimeMs= */ true);
|
||||||
mediaClock.stop();
|
mediaClock.stop();
|
||||||
|
@ -38,6 +38,7 @@ import androidx.media3.exoplayer.source.SampleStream;
|
|||||||
import androidx.media3.exoplayer.text.TextRenderer;
|
import androidx.media3.exoplayer.text.TextRenderer;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
|
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
|
||||||
|
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
@ -784,6 +785,19 @@ import java.util.Objects;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setVideoFrameMetadataListener(VideoFrameMetadataListener videoFrameMetadataListener)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
if (getTrackType() != TRACK_TYPE_VIDEO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
primaryRenderer.handleMessage(
|
||||||
|
Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER, videoFrameMetadataListener);
|
||||||
|
if (secondaryRenderer != null) {
|
||||||
|
secondaryRenderer.handleMessage(
|
||||||
|
Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER, videoFrameMetadataListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets the volume on the renderer. */
|
/** Sets the volume on the renderer. */
|
||||||
public void setVolume(float volume) throws ExoPlaybackException {
|
public void setVolume(float volume) throws ExoPlaybackException {
|
||||||
if (getTrackType() != TRACK_TYPE_AUDIO) {
|
if (getTrackType() != TRACK_TYPE_AUDIO) {
|
||||||
|
@ -622,6 +622,12 @@ public class SimpleExoPlayer extends BasePlayer implements ExoPlayer {
|
|||||||
player.setSkipSilenceEnabled(skipSilenceEnabled);
|
player.setSkipSilenceEnabled(skipSilenceEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScrubbingModeEnabled(boolean scrubbingModeEnabled) {
|
||||||
|
blockUntilConstructorFinished();
|
||||||
|
player.setScrubbingModeEnabled(scrubbingModeEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AnalyticsCollector getAnalyticsCollector() {
|
public AnalyticsCollector getAnalyticsCollector() {
|
||||||
blockUntilConstructorFinished();
|
blockUntilConstructorFinished();
|
||||||
|
@ -741,6 +741,8 @@ public class EventLogger implements AnalyticsListener {
|
|||||||
return "TRANSIENT_AUDIO_FOCUS_LOSS";
|
return "TRANSIENT_AUDIO_FOCUS_LOSS";
|
||||||
case Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT:
|
case Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT:
|
||||||
return "UNSUITABLE_AUDIO_OUTPUT";
|
return "UNSUITABLE_AUDIO_OUTPUT";
|
||||||
|
case Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING:
|
||||||
|
return "SCRUBBING";
|
||||||
default:
|
default:
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.media3.exoplayer;
|
||||||
|
|
||||||
|
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
|
||||||
|
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.advance;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.atLeastOnce;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.view.Surface;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.Player.PositionInfo;
|
||||||
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||||
|
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||||
|
import androidx.media3.test.utils.ExoPlayerTestRunner;
|
||||||
|
import androidx.media3.test.utils.FakeMediaPeriod.TrackDataFactory;
|
||||||
|
import androidx.media3.test.utils.FakeMediaSource;
|
||||||
|
import androidx.media3.test.utils.FakeRenderer;
|
||||||
|
import androidx.media3.test.utils.FakeTimeline;
|
||||||
|
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
|
||||||
|
import androidx.media3.test.utils.TestExoPlayerBuilder;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
/** Tests for {@linkplain ExoPlayer#setScrubbingModeEnabled(boolean) scrubbing mode}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class ExoPlayerScrubbingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scrubbingMode_suppressesPlayback() throws Exception {
|
||||||
|
Timeline timeline = new FakeTimeline();
|
||||||
|
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
|
||||||
|
ExoPlayer player =
|
||||||
|
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
|
||||||
|
.setRenderers(renderer)
|
||||||
|
.build();
|
||||||
|
Player.Listener mockListener = mock(Player.Listener.class);
|
||||||
|
player.addListener(mockListener);
|
||||||
|
|
||||||
|
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
|
||||||
|
player.prepare();
|
||||||
|
player.play();
|
||||||
|
|
||||||
|
advance(player).untilPosition(0, 2000);
|
||||||
|
|
||||||
|
player.setScrubbingModeEnabled(true);
|
||||||
|
verify(mockListener)
|
||||||
|
.onPlaybackSuppressionReasonChanged(Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING);
|
||||||
|
|
||||||
|
player.setScrubbingModeEnabled(false);
|
||||||
|
verify(mockListener)
|
||||||
|
.onPlaybackSuppressionReasonChanged(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
|
||||||
|
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scrubbingMode_pendingSeekIsNotPreempted() throws Exception {
|
||||||
|
Timeline timeline =
|
||||||
|
new FakeTimeline(
|
||||||
|
new TimelineWindowDefinition.Builder().setWindowPositionInFirstPeriodUs(0).build());
|
||||||
|
ExoPlayer player =
|
||||||
|
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
|
||||||
|
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
|
||||||
|
player.setVideoSurface(surface);
|
||||||
|
Player.Listener mockListener = mock(Player.Listener.class);
|
||||||
|
player.addListener(mockListener);
|
||||||
|
|
||||||
|
player.setMediaSource(
|
||||||
|
new FakeMediaSource(
|
||||||
|
timeline,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
TrackDataFactory.samplesWithRateDurationAndKeyframeInterval(
|
||||||
|
/* initialSampleTimeUs= */ 0,
|
||||||
|
/* sampleRate= */ 30,
|
||||||
|
/* durationUs= */ DEFAULT_WINDOW_DURATION_US,
|
||||||
|
/* keyFrameInterval= */ 60),
|
||||||
|
ExoPlayerTestRunner.VIDEO_FORMAT));
|
||||||
|
player.prepare();
|
||||||
|
player.play();
|
||||||
|
|
||||||
|
advance(player).untilPosition(0, 1000);
|
||||||
|
|
||||||
|
VideoFrameMetadataListener mockVideoFrameMetadataListener =
|
||||||
|
mock(VideoFrameMetadataListener.class);
|
||||||
|
player.setVideoFrameMetadataListener(mockVideoFrameMetadataListener);
|
||||||
|
player.setScrubbingModeEnabled(true);
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
|
player.seekTo(2500);
|
||||||
|
player.seekTo(3000);
|
||||||
|
player.seekTo(3500);
|
||||||
|
// Allow the 2500 and 3500 seeks to complete (the 3000 seek should be dropped).
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
|
|
||||||
|
player.seekTo(4000);
|
||||||
|
player.seekTo(4500);
|
||||||
|
// Disabling scrubbing mode should immediately execute the last received seek (pre-empting a
|
||||||
|
// previous one), so we expect the 4500 seek to be resolved and the 4000 seek to be dropped.
|
||||||
|
player.setScrubbingModeEnabled(false);
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
|
player.clearVideoFrameMetadataListener(mockVideoFrameMetadataListener);
|
||||||
|
|
||||||
|
advance(player).untilState(Player.STATE_ENDED);
|
||||||
|
player.release();
|
||||||
|
surface.release();
|
||||||
|
|
||||||
|
ArgumentCaptor<Long> presentationTimeUsCaptor = ArgumentCaptor.forClass(Long.class);
|
||||||
|
verify(mockVideoFrameMetadataListener, atLeastOnce())
|
||||||
|
.onVideoFrameAboutToBeRendered(presentationTimeUsCaptor.capture(), anyLong(), any(), any());
|
||||||
|
|
||||||
|
assertThat(presentationTimeUsCaptor.getAllValues())
|
||||||
|
.containsExactly(2_500_000L, 3_500_000L, 4_500_000L)
|
||||||
|
.inOrder();
|
||||||
|
|
||||||
|
// Confirm that even though we dropped some intermediate seeks, every seek request still
|
||||||
|
// resulted in a position discontinuity callback.
|
||||||
|
ArgumentCaptor<PositionInfo> newPositionCaptor = ArgumentCaptor.forClass(PositionInfo.class);
|
||||||
|
verify(mockListener, atLeastOnce())
|
||||||
|
.onPositionDiscontinuity(
|
||||||
|
/* oldPosition= */ any(),
|
||||||
|
newPositionCaptor.capture(),
|
||||||
|
eq(Player.DISCONTINUITY_REASON_SEEK));
|
||||||
|
assertThat(newPositionCaptor.getAllValues().stream().map(p -> p.positionMs))
|
||||||
|
.containsExactly(2500L, 3000L, 3500L, 4000L, 4500L)
|
||||||
|
.inOrder();
|
||||||
|
}
|
||||||
|
}
|
@ -215,6 +215,11 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer {
|
|||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScrubbingModeEnabled(boolean scrubbingModeEnabled) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setVideoEffects(List<Effect> videoEffects) {
|
public void setVideoEffects(List<Effect> videoEffects) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user