mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +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
|
being enabled). Any changes made to the Player outside of the
|
||||||
observation period are now picked up
|
observation period are now picked up
|
||||||
([#2313](https://github.com/androidx/media/issues/2313)).
|
([#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:
|
* Downloads:
|
||||||
* Add partial download support for progressive streams. Apps can prepare a
|
* Add partial download support for progressive streams. Apps can prepare a
|
||||||
progressive stream with `DownloadHelper`, and request 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_IDLE
|
||||||
|| player.getPlaybackState() == Player.STATE_ENDED
|
|| player.getPlaybackState() == Player.STATE_ENDED
|
||||||
|| (shouldShowPlayIfSuppressed
|
|| (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 {}
|
-keepnames class androidx.media3.exoplayer.ExoPlayer {}
|
||||||
-keepclassmembers class androidx.media3.exoplayer.ExoPlayer {
|
-keepclassmembers class androidx.media3.exoplayer.ExoPlayer {
|
||||||
void setImageOutput(androidx.media3.exoplayer.image.ImageOutput);
|
void setImageOutput(androidx.media3.exoplayer.image.ImageOutput);
|
||||||
|
void setScrubbingModeEnabled(boolean);
|
||||||
|
boolean isScrubbingModeEnabled();
|
||||||
}
|
}
|
||||||
-keepclasseswithmembers class androidx.media3.exoplayer.image.ImageOutput {
|
-keepclasseswithmembers class androidx.media3.exoplayer.image.ImageOutput {
|
||||||
void onImageAvailable(long, android.graphics.Bitmap);
|
void onImageAvailable(long, android.graphics.Bitmap);
|
||||||
|
@ -79,12 +79,15 @@ import androidx.media3.common.TrackSelectionOverride;
|
|||||||
import androidx.media3.common.TrackSelectionParameters;
|
import androidx.media3.common.TrackSelectionParameters;
|
||||||
import androidx.media3.common.Tracks;
|
import androidx.media3.common.Tracks;
|
||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
|
import androidx.media3.common.util.Log;
|
||||||
import androidx.media3.common.util.RepeatModeUtil;
|
import androidx.media3.common.util.RepeatModeUtil;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -92,6 +95,7 @@ import java.util.Formatter;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view for controlling {@link Player} instances.
|
* A view for controlling {@link Player} instances.
|
||||||
@ -156,6 +160,14 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
* <li>Corresponding method: {@link #setAnimationEnabled(boolean)}
|
* <li>Corresponding method: {@link #setAnimationEnabled(boolean)}
|
||||||
* <li>Default: true
|
* <li>Default: true
|
||||||
* </ul>
|
* </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
|
* <li><b>{@code time_bar_min_update_interval}</b> - Specifies the minimum interval between time
|
||||||
* bar position updates.
|
* bar position updates.
|
||||||
* <ul>
|
* <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. */
|
/** 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;
|
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. */
|
/** The maximum interval between time bar position updates. */
|
||||||
private static final int MAX_UPDATE_INTERVAL_MS = 1_000;
|
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 PlayerControlViewLayoutManager controlViewLayoutManager;
|
||||||
private final Resources resources;
|
private final Resources resources;
|
||||||
private final ComponentListener componentListener;
|
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.
|
@SuppressWarnings("deprecation") // Using the deprecated type for now.
|
||||||
private final CopyOnWriteArrayList<VisibilityListener> visibilityListeners;
|
private final CopyOnWriteArrayList<VisibilityListener> visibilityListeners;
|
||||||
@ -442,6 +459,7 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
private boolean multiWindowTimeBar;
|
private boolean multiWindowTimeBar;
|
||||||
private boolean scrubbing;
|
private boolean scrubbing;
|
||||||
private int showTimeoutMs;
|
private int showTimeoutMs;
|
||||||
|
private boolean timeBarScrubbingEnabled;
|
||||||
private int timeBarMinUpdateIntervalMs;
|
private int timeBarMinUpdateIntervalMs;
|
||||||
private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
|
private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
|
||||||
private long[] adGroupTimesMs;
|
private long[] adGroupTimesMs;
|
||||||
@ -572,6 +590,8 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
showSubtitleButton =
|
showSubtitleButton =
|
||||||
a.getBoolean(R.styleable.PlayerControlView_show_subtitle_button, showSubtitleButton);
|
a.getBoolean(R.styleable.PlayerControlView_show_subtitle_button, showSubtitleButton);
|
||||||
showVrButton = a.getBoolean(R.styleable.PlayerControlView_show_vr_button, showVrButton);
|
showVrButton = a.getBoolean(R.styleable.PlayerControlView_show_vr_button, showVrButton);
|
||||||
|
timeBarScrubbingEnabled =
|
||||||
|
a.getBoolean(R.styleable.PlayerControlView_time_bar_scrubbing_enabled, false);
|
||||||
setTimeBarMinUpdateInterval(
|
setTimeBarMinUpdateInterval(
|
||||||
a.getInt(
|
a.getInt(
|
||||||
R.styleable.PlayerControlView_time_bar_min_update_interval,
|
R.styleable.PlayerControlView_time_bar_min_update_interval,
|
||||||
@ -598,6 +618,21 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
extraPlayedAdGroups = new boolean[0];
|
extraPlayedAdGroups = new boolean[0];
|
||||||
updateProgressAction = this::updateProgress;
|
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);
|
durationView = findViewById(R.id.exo_duration);
|
||||||
positionView = findViewById(R.id.exo_position);
|
positionView = findViewById(R.id.exo_position);
|
||||||
|
|
||||||
@ -1088,6 +1123,17 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
return controlViewLayoutManager.isAnimationEnabled();
|
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.
|
* 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));
|
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
|
||||||
}
|
}
|
||||||
controlViewLayoutManager.removeHideCallbacks();
|
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
|
@Override
|
||||||
@ -1859,17 +1920,45 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
if (positionView != null) {
|
if (positionView != null) {
|
||||||
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
|
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
|
@Override
|
||||||
public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
|
public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
|
||||||
scrubbing = false;
|
scrubbing = false;
|
||||||
if (!canceled && player != null) {
|
if (player != null) {
|
||||||
|
if (!canceled) {
|
||||||
seekToTimeBarPosition(player, position);
|
seekToTimeBarPosition(player, position);
|
||||||
}
|
}
|
||||||
|
if (isExoPlayer(player)) {
|
||||||
|
try {
|
||||||
|
checkNotNull(setScrubbingModeEnabledMethod).invoke(player, false);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
controlViewLayoutManager.resetHideCallbacks();
|
controlViewLayoutManager.resetHideCallbacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(result = true, expression = "#1")
|
||||||
|
private boolean isExoPlayer(@Nullable Player player) {
|
||||||
|
return player != null
|
||||||
|
&& exoplayerClazz != null
|
||||||
|
&& exoplayerClazz.isAssignableFrom(player.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDismiss() {
|
public void onDismiss() {
|
||||||
if (needToHideBars) {
|
if (needToHideBars) {
|
||||||
|
@ -1283,6 +1283,19 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
|
|||||||
controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
|
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
|
* Sets whether a play button is shown if playback is {@linkplain
|
||||||
* Player#getPlaybackSuppressionReason() suppressed}.
|
* Player#getPlaybackSuppressionReason() suppressed}.
|
||||||
|
@ -90,6 +90,7 @@
|
|||||||
<attr name="subtitle_off_icon" format="reference"/>
|
<attr name="subtitle_off_icon" format="reference"/>
|
||||||
<attr name="show_vr_button" format="boolean"/>
|
<attr name="show_vr_button" format="boolean"/>
|
||||||
<attr name="vr_icon" format="reference"/>
|
<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="time_bar_min_update_interval" format="integer"/>
|
||||||
<attr name="controller_layout_id" format="reference"/>
|
<attr name="controller_layout_id" format="reference"/>
|
||||||
<attr name="animation_enabled" format="boolean"/>
|
<attr name="animation_enabled" format="boolean"/>
|
||||||
@ -146,6 +147,7 @@
|
|||||||
<attr name="subtitle_on_icon"/>
|
<attr name="subtitle_on_icon"/>
|
||||||
<attr name="show_vr_button"/>
|
<attr name="show_vr_button"/>
|
||||||
<attr name="vr_icon"/>
|
<attr name="vr_icon"/>
|
||||||
|
<attr name="time_bar_scrubbing_enabled"/>
|
||||||
<attr name="time_bar_min_update_interval"/>
|
<attr name="time_bar_min_update_interval"/>
|
||||||
<attr name="controller_layout_id"/>
|
<attr name="controller_layout_id"/>
|
||||||
<attr name="animation_enabled"/>
|
<attr name="animation_enabled"/>
|
||||||
@ -227,6 +229,7 @@
|
|||||||
<attr name="subtitle_off_icon"/>
|
<attr name="subtitle_off_icon"/>
|
||||||
<attr name="show_vr_button"/>
|
<attr name="show_vr_button"/>
|
||||||
<attr name="vr_icon"/>
|
<attr name="vr_icon"/>
|
||||||
|
<attr name="time_bar_scrubbing_enabled"/>
|
||||||
<attr name="time_bar_min_update_interval"/>
|
<attr name="time_bar_min_update_interval"/>
|
||||||
<attr name="controller_layout_id"/>
|
<attr name="controller_layout_id"/>
|
||||||
<attr name="animation_enabled"/>
|
<attr name="animation_enabled"/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user