diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b8905d66dc..cedebcb51b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 4de9b0ca95..6734f41d0f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -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); } /** diff --git a/libraries/ui/proguard-rules.txt b/libraries/ui/proguard-rules.txt index c6f7553c44..288c5d050a 100644 --- a/libraries/ui/proguard-rules.txt +++ b/libraries/ui/proguard-rules.txt @@ -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); diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java index 5456b4847f..f901b26825 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java @@ -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; *
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) { diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java index 8ea0c175ce..b25ed50ef0 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java @@ -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). + * + *
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}.
diff --git a/libraries/ui/src/main/res/values/attrs.xml b/libraries/ui/src/main/res/values/attrs.xml
index 38d090bba8..55694a3ddf 100644
--- a/libraries/ui/src/main/res/values/attrs.xml
+++ b/libraries/ui/src/main/res/values/attrs.xml
@@ -90,6 +90,7 @@