mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Integrate PlayerControlView
with new scrubbing mode
This also tweaks the logic in `Util.shouldShowPlayButton` to special-case the 'scrubbing' suppression reason. In most cases of playback suppression (e.g. transient loss of audio focus due to a phone call), the recommended UX is to show a 'play' button (i.e. the UI looks like the player is paused). This doesn't look right when scrubbing since although 'ongoing' playback is suppressed, the image on screen is constantly changing, so a 'pause' button is kept (i.e. the UI looks like the player is playing). PiperOrigin-RevId: 751385521
This commit is contained in:
parent
20ab1ea8e5
commit
49b57b8da3
@ -64,6 +64,13 @@
|
||||
being enabled). Any changes made to the Player outside of the
|
||||
observation period are now picked up
|
||||
([#2313](https://github.com/androidx/media/issues/2313)).
|
||||
* Add support for ExoPlayer's scrubbing mode to `PlayerControlView`. When
|
||||
enabled, this puts the player into scrubbing mode when the user starts
|
||||
dragging the scrubber bar, issues a `player.seekTo` call for every
|
||||
movement, and then exits scrubbing mode when the touch is lifted from
|
||||
the screen. This integration can be enabled with either
|
||||
`time_bar_scrubbing_enabled = true` in XML or the
|
||||
`setTimeBarScrubbingEnabled(boolean)` method from Java/Kotlin.
|
||||
* Downloads:
|
||||
* Add partial download support for progressive streams. Apps can prepare a
|
||||
progressive stream with `DownloadHelper`, and request a
|
||||
|
@ -3713,7 +3713,9 @@ public final class Util {
|
||||
|| player.getPlaybackState() == Player.STATE_IDLE
|
||||
|| player.getPlaybackState() == Player.STATE_ENDED
|
||||
|| (shouldShowPlayIfSuppressed
|
||||
&& player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE);
|
||||
&& player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
&& player.getPlaybackSuppressionReason()
|
||||
!= Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,6 +12,8 @@
|
||||
-keepnames class androidx.media3.exoplayer.ExoPlayer {}
|
||||
-keepclassmembers class androidx.media3.exoplayer.ExoPlayer {
|
||||
void setImageOutput(androidx.media3.exoplayer.image.ImageOutput);
|
||||
void setScrubbingModeEnabled(boolean);
|
||||
boolean isScrubbingModeEnabled();
|
||||
}
|
||||
-keepclasseswithmembers class androidx.media3.exoplayer.image.ImageOutput {
|
||||
void onImageAvailable(long, android.graphics.Bitmap);
|
||||
|
@ -79,12 +79,15 @@ import androidx.media3.common.TrackSelectionOverride;
|
||||
import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.RepeatModeUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -92,6 +95,7 @@ import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||
|
||||
/**
|
||||
* A view for controlling {@link Player} instances.
|
||||
@ -156,6 +160,14 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
* <li>Corresponding method: {@link #setAnimationEnabled(boolean)}
|
||||
* <li>Default: true
|
||||
* </ul>
|
||||
* <li><b>{@code time_bar_scrubbing_enabled}</b> - Whether the time bar should {@linkplain
|
||||
* Player#seekTo seek} immediately as the user drags the scrubber around (true), or only seek
|
||||
* when the user releases the scrubber (false). This can only be used if the {@linkplain
|
||||
* #setPlayer connected player} is an instance of {@code androidx.media3.exoplayer.ExoPlayer}.
|
||||
* <ul>
|
||||
* <li>Corresponding method: {@link #setTimeBarScrubbingEnabled(boolean)}
|
||||
* <li>Default: {@code false}
|
||||
* </ul>
|
||||
* <li><b>{@code time_bar_min_update_interval}</b> - Specifies the minimum interval between time
|
||||
* bar position updates.
|
||||
* <ul>
|
||||
@ -352,6 +364,8 @@ public class PlayerControlView extends FrameLayout {
|
||||
/** The maximum number of windows that can be shown in a multi-window time bar. */
|
||||
public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;
|
||||
|
||||
private static final String TAG = "PlayerControlView";
|
||||
|
||||
/** The maximum interval between time bar position updates. */
|
||||
private static final int MAX_UPDATE_INTERVAL_MS = 1_000;
|
||||
|
||||
@ -366,6 +380,9 @@ public class PlayerControlView extends FrameLayout {
|
||||
private final PlayerControlViewLayoutManager controlViewLayoutManager;
|
||||
private final Resources resources;
|
||||
private final ComponentListener componentListener;
|
||||
@Nullable private final Class<?> exoplayerClazz;
|
||||
@Nullable private final Method setScrubbingModeEnabledMethod;
|
||||
@Nullable private final Method isScrubbingModeEnabledMethod;
|
||||
|
||||
@SuppressWarnings("deprecation") // Using the deprecated type for now.
|
||||
private final CopyOnWriteArrayList<VisibilityListener> visibilityListeners;
|
||||
@ -442,6 +459,7 @@ public class PlayerControlView extends FrameLayout {
|
||||
private boolean multiWindowTimeBar;
|
||||
private boolean scrubbing;
|
||||
private int showTimeoutMs;
|
||||
private boolean timeBarScrubbingEnabled;
|
||||
private int timeBarMinUpdateIntervalMs;
|
||||
private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
|
||||
private long[] adGroupTimesMs;
|
||||
@ -572,6 +590,8 @@ public class PlayerControlView extends FrameLayout {
|
||||
showSubtitleButton =
|
||||
a.getBoolean(R.styleable.PlayerControlView_show_subtitle_button, showSubtitleButton);
|
||||
showVrButton = a.getBoolean(R.styleable.PlayerControlView_show_vr_button, showVrButton);
|
||||
timeBarScrubbingEnabled =
|
||||
a.getBoolean(R.styleable.PlayerControlView_time_bar_scrubbing_enabled, false);
|
||||
setTimeBarMinUpdateInterval(
|
||||
a.getInt(
|
||||
R.styleable.PlayerControlView_time_bar_min_update_interval,
|
||||
@ -598,6 +618,21 @@ public class PlayerControlView extends FrameLayout {
|
||||
extraPlayedAdGroups = new boolean[0];
|
||||
updateProgressAction = this::updateProgress;
|
||||
|
||||
Class<?> exoplayerClazz = null;
|
||||
Method setScrubbingModeEnabledMethod = null;
|
||||
Method isScrubbingModeEnabledMethod = null;
|
||||
try {
|
||||
exoplayerClazz = Class.forName("androidx.media3.exoplayer.ExoPlayer");
|
||||
setScrubbingModeEnabledMethod =
|
||||
exoplayerClazz.getMethod("setScrubbingModeEnabled", boolean.class);
|
||||
isScrubbingModeEnabledMethod = exoplayerClazz.getMethod("isScrubbingModeEnabled");
|
||||
} catch (ClassNotFoundException | NoSuchMethodException e) {
|
||||
// Expected if ExoPlayer module not available.
|
||||
}
|
||||
this.exoplayerClazz = exoplayerClazz;
|
||||
this.setScrubbingModeEnabledMethod = setScrubbingModeEnabledMethod;
|
||||
this.isScrubbingModeEnabledMethod = isScrubbingModeEnabledMethod;
|
||||
|
||||
durationView = findViewById(R.id.exo_duration);
|
||||
positionView = findViewById(R.id.exo_position);
|
||||
|
||||
@ -1088,6 +1123,17 @@ public class PlayerControlView extends FrameLayout {
|
||||
return controlViewLayoutManager.isAnimationEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the time bar should {@linkplain Player#seekTo seek} immediately as the user drags
|
||||
* the scrubber around (true), or only seek when the user releases the scrubber (false).
|
||||
*
|
||||
* <p>This can only be used if the {@linkplain #setPlayer connected player} is an instance of
|
||||
* {@code androidx.media3.exoplayer.ExoPlayer}.
|
||||
*/
|
||||
public void setTimeBarScrubbingEnabled(boolean timeBarScrubbingEnabled) {
|
||||
this.timeBarScrubbingEnabled = timeBarScrubbingEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum interval between time bar position updates.
|
||||
*
|
||||
@ -1852,6 +1898,21 @@ public class PlayerControlView extends FrameLayout {
|
||||
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
|
||||
}
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
if (player != null && timeBarScrubbingEnabled) {
|
||||
if (isExoPlayer(player)) {
|
||||
try {
|
||||
checkNotNull(setScrubbingModeEnabledMethod).invoke(player, true);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Time bar scrubbing is enabled, but player is not an ExoPlayer instance, so ignoring"
|
||||
+ " (because we can't enable scrubbing mode). player.class="
|
||||
+ checkNotNull(player).getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1859,17 +1920,45 @@ public class PlayerControlView extends FrameLayout {
|
||||
if (positionView != null) {
|
||||
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
|
||||
}
|
||||
boolean isScrubbingModeEnabled;
|
||||
try {
|
||||
isScrubbingModeEnabled =
|
||||
isExoPlayer(player)
|
||||
&& (boolean)
|
||||
checkNotNull(checkNotNull(isScrubbingModeEnabledMethod).invoke(player));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (isScrubbingModeEnabled) {
|
||||
seekToTimeBarPosition(checkNotNull(player), position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
|
||||
scrubbing = false;
|
||||
if (!canceled && player != null) {
|
||||
seekToTimeBarPosition(player, position);
|
||||
if (player != null) {
|
||||
if (!canceled) {
|
||||
seekToTimeBarPosition(player, position);
|
||||
}
|
||||
if (isExoPlayer(player)) {
|
||||
try {
|
||||
checkNotNull(setScrubbingModeEnabledMethod).invoke(player, false);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
controlViewLayoutManager.resetHideCallbacks();
|
||||
}
|
||||
|
||||
@EnsuresNonNullIf(result = true, expression = "#1")
|
||||
private boolean isExoPlayer(@Nullable Player player) {
|
||||
return player != null
|
||||
&& exoplayerClazz != null
|
||||
&& exoplayerClazz.isAssignableFrom(player.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
if (needToHideBars) {
|
||||
|
@ -1283,6 +1283,19 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
|
||||
controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the time bar should {@linkplain Player#seekTo seek} immediately as the user drags
|
||||
* the scrubber around (true), or only seek when the user releases the scrubber (false).
|
||||
*
|
||||
* <p>This can only be used if the {@linkplain #setPlayer connected player} is an instance of
|
||||
* {@code androidx.media3.exoplayer.ExoPlayer}.
|
||||
*/
|
||||
@UnstableApi
|
||||
public void setTimeBarScrubbingEnabled(boolean timeBarScrubbingEnabled) {
|
||||
Assertions.checkStateNotNull(controller);
|
||||
controller.setTimeBarScrubbingEnabled(timeBarScrubbingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether a play button is shown if playback is {@linkplain
|
||||
* Player#getPlaybackSuppressionReason() suppressed}.
|
||||
|
@ -90,6 +90,7 @@
|
||||
<attr name="subtitle_off_icon" format="reference"/>
|
||||
<attr name="show_vr_button" format="boolean"/>
|
||||
<attr name="vr_icon" format="reference"/>
|
||||
<attr name="time_bar_scrubbing_enabled" format="boolean"/>
|
||||
<attr name="time_bar_min_update_interval" format="integer"/>
|
||||
<attr name="controller_layout_id" format="reference"/>
|
||||
<attr name="animation_enabled" format="boolean"/>
|
||||
@ -146,6 +147,7 @@
|
||||
<attr name="subtitle_on_icon"/>
|
||||
<attr name="show_vr_button"/>
|
||||
<attr name="vr_icon"/>
|
||||
<attr name="time_bar_scrubbing_enabled"/>
|
||||
<attr name="time_bar_min_update_interval"/>
|
||||
<attr name="controller_layout_id"/>
|
||||
<attr name="animation_enabled"/>
|
||||
@ -227,6 +229,7 @@
|
||||
<attr name="subtitle_off_icon"/>
|
||||
<attr name="show_vr_button"/>
|
||||
<attr name="vr_icon"/>
|
||||
<attr name="time_bar_scrubbing_enabled"/>
|
||||
<attr name="time_bar_min_update_interval"/>
|
||||
<attr name="controller_layout_id"/>
|
||||
<attr name="animation_enabled"/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user