mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Compare commits
7 Commits
20ab1ea8e5
...
170098b400
Author | SHA1 | Date | |
---|---|---|---|
![]() |
170098b400 | ||
![]() |
35303c94a1 | ||
![]() |
09ce64ec21 | ||
![]() |
8bf658cd79 | ||
![]() |
fe59718805 | ||
![]() |
ae7d7dc7e8 | ||
![]() |
49b57b8da3 |
@ -40,6 +40,8 @@
|
||||
* Improve codec performance checks for software video codecs. This may
|
||||
lead to some additional tracks being marked as `EXCEEDS_CAPABILITIES`.
|
||||
* Text:
|
||||
* Fix SSA and SubRip to display an in-progress cue when enabling subtitles
|
||||
([#2309](https://github.com/androidx/media/issues/2309)).
|
||||
* Metadata:
|
||||
* Image:
|
||||
* DataSource:
|
||||
@ -64,6 +66,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
|
||||
|
@ -15,10 +15,12 @@
|
||||
*/
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
@ -40,6 +42,7 @@ import androidx.media3.common.TrackSelectionParameters;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSchemeDataSource;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
@ -502,6 +505,25 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
lastSeenTracks = tracks;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class) // For PlayerView.setTimeBarScrubbingEnabled
|
||||
@Override
|
||||
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
|
||||
if (playerView == null) {
|
||||
return;
|
||||
}
|
||||
if (mediaItem == null) {
|
||||
playerView.setTimeBarScrubbingEnabled(false);
|
||||
return;
|
||||
}
|
||||
String uriScheme = mediaItem.localConfiguration.uri.getScheme();
|
||||
playerView.setTimeBarScrubbingEnabled(
|
||||
TextUtils.isEmpty(uriScheme)
|
||||
|| uriScheme.equals(ContentResolver.SCHEME_FILE)
|
||||
|| uriScheme.equals("asset")
|
||||
|| uriScheme.equals(DataSchemeDataSource.SCHEME_DATA)
|
||||
|| uriScheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE));
|
||||
}
|
||||
}
|
||||
|
||||
private class PlayerErrorMessageProvider implements ErrorMessageProvider<PlaybackException> {
|
||||
|
@ -47,6 +47,7 @@ import android.app.Service;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
@ -419,7 +420,8 @@ public final class Util {
|
||||
}
|
||||
|
||||
private static boolean isMediaStoreExternalContentUri(Uri uri) {
|
||||
if (!"content".equals(uri.getScheme()) || !MediaStore.AUTHORITY.equals(uri.getAuthority())) {
|
||||
if (!Objects.equals(uri.getScheme(), ContentResolver.SCHEME_CONTENT)
|
||||
|| !Objects.equals(uri.getAuthority(), MediaStore.AUTHORITY)) {
|
||||
return false;
|
||||
}
|
||||
List<String> pathSegments = uri.getPathSegments();
|
||||
@ -467,7 +469,7 @@ public final class Util {
|
||||
@UnstableApi
|
||||
public static boolean isLocalFileUri(Uri uri) {
|
||||
String scheme = uri.getScheme();
|
||||
return TextUtils.isEmpty(scheme) || "file".equals(scheme);
|
||||
return TextUtils.isEmpty(scheme) || Objects.equals(scheme, ContentResolver.SCHEME_FILE);
|
||||
}
|
||||
|
||||
/** Returns true if the code path is currently running on an emulator. */
|
||||
@ -3713,7 +3715,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,6 +32,7 @@ import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Objects;
|
||||
|
||||
/** A {@link DataSource} for reading from a content URI. */
|
||||
@UnstableApi
|
||||
@ -81,7 +82,7 @@ public final class ContentDataSource extends BaseDataSource {
|
||||
transferInitializing(dataSpec);
|
||||
|
||||
AssetFileDescriptor assetFileDescriptor;
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
if (Objects.equals(uri.getScheme(), ContentResolver.SCHEME_CONTENT)) {
|
||||
Bundle providerOptions = new Bundle();
|
||||
// We don't want compatible media transcoding.
|
||||
providerOptions.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, true);
|
||||
|
@ -122,7 +122,7 @@ public final class DefaultDataSource implements DataSource {
|
||||
private static final String TAG = "DefaultDataSource";
|
||||
|
||||
private static final String SCHEME_ASSET = "asset";
|
||||
private static final String SCHEME_CONTENT = "content";
|
||||
private static final String SCHEME_CONTENT = ContentResolver.SCHEME_CONTENT;
|
||||
private static final String SCHEME_RTMP = "rtmp";
|
||||
private static final String SCHEME_UDP = "udp";
|
||||
private static final String SCHEME_DATA = DataSchemeDataSource.SCHEME_DATA;
|
||||
|
@ -46,6 +46,7 @@ import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.MediaFormatUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DataSourceUtil;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
@ -287,11 +288,8 @@ public final class MediaExtractorCompat {
|
||||
*/
|
||||
public void setDataSource(Context context, Uri uri, @Nullable Map<String, String> headers)
|
||||
throws IOException {
|
||||
String scheme = uri.getScheme();
|
||||
String path = uri.getPath();
|
||||
if ((scheme == null || scheme.equals("file")) && path != null) {
|
||||
// If the URI scheme is null or file, treat it as a local file path
|
||||
setDataSource(path);
|
||||
if (Util.isLocalFileUri(uri)) {
|
||||
setDataSource(checkNotNull(uri.getPath()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -259,7 +259,7 @@ public class SubtitleExtractor implements Extractor {
|
||||
cuesWithTiming.startTimeUs,
|
||||
cueEncoder.encode(cuesWithTiming.cues, cuesWithTiming.durationUs));
|
||||
samples.add(sample);
|
||||
if (seekTimeUs == C.TIME_UNSET || cuesWithTiming.startTimeUs >= seekTimeUs) {
|
||||
if (seekTimeUs == C.TIME_UNSET || cuesWithTiming.endTimeUs >= seekTimeUs) {
|
||||
writeToOutput(sample);
|
||||
}
|
||||
});
|
||||
|
@ -167,13 +167,14 @@ public final class SsaParser implements SubtitleParser {
|
||||
}
|
||||
long startTimeUs = startTimesUs.get(i);
|
||||
// It's safe to inspect element i+1, because we already exited the loop above if i=size()-1.
|
||||
long durationUs = startTimesUs.get(i + 1) - startTimesUs.get(i);
|
||||
if (outputOptions.startTimeUs == C.TIME_UNSET || startTimeUs >= outputOptions.startTimeUs) {
|
||||
output.accept(new CuesWithTiming(cuesForThisStartTime, startTimeUs, durationUs));
|
||||
|
||||
long endTimeUs = startTimesUs.get(i + 1);
|
||||
CuesWithTiming cuesWithTiming =
|
||||
new CuesWithTiming(
|
||||
cuesForThisStartTime, startTimeUs, /* durationUs= */ endTimeUs - startTimeUs);
|
||||
if (outputOptions.startTimeUs == C.TIME_UNSET || endTimeUs >= outputOptions.startTimeUs) {
|
||||
output.accept(cuesWithTiming);
|
||||
} else if (cuesWithTimingBeforeRequestedStartTimeUs != null) {
|
||||
cuesWithTimingBeforeRequestedStartTimeUs.add(
|
||||
new CuesWithTiming(cuesForThisStartTime, startTimeUs, durationUs));
|
||||
cuesWithTimingBeforeRequestedStartTimeUs.add(cuesWithTiming);
|
||||
}
|
||||
}
|
||||
if (cuesWithTimingBeforeRequestedStartTimeUs != null) {
|
||||
|
@ -166,7 +166,7 @@ public final class SubripParser implements SubtitleParser {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (outputOptions.startTimeUs == C.TIME_UNSET || startTimeUs >= outputOptions.startTimeUs) {
|
||||
if (outputOptions.startTimeUs == C.TIME_UNSET || endTimeUs >= outputOptions.startTimeUs) {
|
||||
output.accept(
|
||||
new CuesWithTiming(
|
||||
ImmutableList.of(buildCue(text, alignmentTag)),
|
||||
|
@ -129,7 +129,8 @@ public final class SsaParserTest {
|
||||
SsaParser parser = new SsaParser();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL);
|
||||
List<CuesWithTiming> cues = new ArrayList<>();
|
||||
parser.parse(bytes, OutputOptions.onlyCuesAfter(/* startTimeUs= */ 1_000_000), cues::add);
|
||||
// Choose a start time halfway through the second cue, and expect it to be included.
|
||||
parser.parse(bytes, OutputOptions.onlyCuesAfter(/* startTimeUs= */ 3_000_000), cues::add);
|
||||
|
||||
assertThat(cues).hasSize(2);
|
||||
assertTypicalCue2(cues.get(0));
|
||||
@ -141,9 +142,10 @@ public final class SsaParserTest {
|
||||
SsaParser parser = new SsaParser();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL);
|
||||
List<CuesWithTiming> cues = new ArrayList<>();
|
||||
// Choose a start time halfway through the second cue, and expect it to be considered 'after'.
|
||||
parser.parse(
|
||||
bytes,
|
||||
OutputOptions.cuesAfterThenRemainingCuesBefore(/* startTimeUs= */ 1_000_000),
|
||||
OutputOptions.cuesAfterThenRemainingCuesBefore(/* startTimeUs= */ 3_000_000),
|
||||
cues::add);
|
||||
|
||||
assertThat(cues).hasSize(3);
|
||||
|
@ -105,7 +105,8 @@ public final class SubripParserTest {
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
|
||||
|
||||
List<CuesWithTiming> cues = new ArrayList<>();
|
||||
parser.parse(bytes, OutputOptions.onlyCuesAfter(/* startTimeUs= */ 1_000_000), cues::add);
|
||||
// Choose a start time halfway through the second cue, and expect it to be included.
|
||||
parser.parse(bytes, OutputOptions.onlyCuesAfter(/* startTimeUs= */ 3_000_000), cues::add);
|
||||
|
||||
assertThat(cues).hasSize(2);
|
||||
assertTypicalCue2(cues.get(0));
|
||||
@ -118,9 +119,10 @@ public final class SubripParserTest {
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
|
||||
|
||||
List<CuesWithTiming> cues = new ArrayList<>();
|
||||
// Choose a start time halfway through the second cue, and expect it to be considered 'after'.
|
||||
parser.parse(
|
||||
bytes,
|
||||
OutputOptions.cuesAfterThenRemainingCuesBefore(/* startTimeUs= */ 1_000_000),
|
||||
OutputOptions.cuesAfterThenRemainingCuesBefore(/* startTimeUs= */ 3_000_000),
|
||||
cues::add);
|
||||
|
||||
assertThat(cues).hasSize(3);
|
||||
|
@ -400,6 +400,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
||||
* @param composition The {@link Composition} to play. Every {@link EditedMediaItem} in the {@link
|
||||
* Composition} must have its {@link EditedMediaItem#durationUs} set.
|
||||
*/
|
||||
@SuppressWarnings("FutureReturnValueIgnored")
|
||||
public void setComposition(Composition composition) {
|
||||
verifyApplicationThread();
|
||||
checkArgument(!composition.sequences.isEmpty());
|
||||
@ -412,20 +413,9 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
||||
}
|
||||
|
||||
setCompositionInternal(composition);
|
||||
if (videoOutput != null) {
|
||||
if (videoOutput instanceof SurfaceHolder) {
|
||||
setVideoSurfaceHolderInternal((SurfaceHolder) videoOutput);
|
||||
} else if (videoOutput instanceof SurfaceView) {
|
||||
SurfaceView surfaceView = (SurfaceView) videoOutput;
|
||||
setVideoSurfaceHolderInternal(surfaceView.getHolder());
|
||||
} else if (videoOutput instanceof Surface) {
|
||||
setVideoSurfaceInternal((Surface) videoOutput, checkNotNull(videoOutputSize));
|
||||
} else {
|
||||
throw new IllegalStateException(videoOutput.getClass().toString());
|
||||
}
|
||||
}
|
||||
// Update the composition field at the end after everything else has been set.
|
||||
this.composition = composition;
|
||||
maybeSetVideoOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -444,7 +434,6 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
||||
}
|
||||
|
||||
/** Sets the {@link Surface} and {@link Size} to render to. */
|
||||
@VisibleForTesting
|
||||
public void setVideoSurface(Surface surface, Size videoOutputSize) {
|
||||
videoOutput = surface;
|
||||
this.videoOutputSize = videoOutputSize;
|
||||
@ -616,18 +605,12 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
||||
@Override
|
||||
protected ListenableFuture<?> handleSetVideoOutput(Object videoOutput) {
|
||||
if (!(videoOutput instanceof SurfaceHolder || videoOutput instanceof SurfaceView)) {
|
||||
throw new UnsupportedOperationException(videoOutput.getClass().toString());
|
||||
throw new UnsupportedOperationException(
|
||||
videoOutput.getClass().toString()
|
||||
+ ". Use CompositionPlayer.setVideoSurface() for Surface output.");
|
||||
}
|
||||
this.videoOutput = videoOutput;
|
||||
if (composition == null) {
|
||||
return Futures.immediateVoidFuture();
|
||||
}
|
||||
if (videoOutput instanceof SurfaceHolder) {
|
||||
setVideoSurfaceHolderInternal((SurfaceHolder) videoOutput);
|
||||
} else {
|
||||
setVideoSurfaceHolderInternal(((SurfaceView) videoOutput).getHolder());
|
||||
}
|
||||
return Futures.immediateVoidFuture();
|
||||
return maybeSetVideoOutput();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1012,6 +995,25 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
||||
};
|
||||
}
|
||||
|
||||
private ListenableFuture<?> maybeSetVideoOutput() {
|
||||
if (videoOutput == null || composition == null) {
|
||||
return Futures.immediateVoidFuture();
|
||||
}
|
||||
if (videoOutput instanceof SurfaceHolder) {
|
||||
setVideoSurfaceHolderInternal((SurfaceHolder) videoOutput);
|
||||
} else if (videoOutput instanceof SurfaceView) {
|
||||
SurfaceView surfaceView = (SurfaceView) videoOutput;
|
||||
setVideoSurfaceHolderInternal(surfaceView.getHolder());
|
||||
} else if (videoOutput instanceof Surface) {
|
||||
setVideoSurfaceInternal(
|
||||
(Surface) videoOutput,
|
||||
checkNotNull(videoOutputSize, "VideoOutputSize must be set when using Surface output"));
|
||||
} else {
|
||||
throw new IllegalStateException(videoOutput.getClass().toString());
|
||||
}
|
||||
return Futures.immediateVoidFuture();
|
||||
}
|
||||
|
||||
private long getContentPositionMs() {
|
||||
if (players.isEmpty()) {
|
||||
return 0;
|
||||
|
@ -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) {
|
||||
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"/>
|
||||
|
@ -57,17 +57,17 @@ fun PlayerSurface(
|
||||
PlayerSurfaceInternal(
|
||||
player,
|
||||
modifier,
|
||||
createView = { SurfaceView(it) },
|
||||
setViewOnPlayer = { player, view -> player.setVideoSurfaceView(view) },
|
||||
clearViewFromPlayer = { player, view -> player.clearVideoSurfaceView(view) },
|
||||
createView = ::SurfaceView,
|
||||
setVideoView = Player::setVideoSurfaceView,
|
||||
clearVideoView = Player::clearVideoSurfaceView,
|
||||
)
|
||||
SURFACE_TYPE_TEXTURE_VIEW ->
|
||||
PlayerSurfaceInternal(
|
||||
player,
|
||||
modifier,
|
||||
createView = { TextureView(it) },
|
||||
setViewOnPlayer = { player, view -> player.setVideoTextureView(view) },
|
||||
clearViewFromPlayer = { player, view -> player.clearVideoTextureView(view) },
|
||||
createView = ::TextureView,
|
||||
setVideoView = Player::setVideoTextureView,
|
||||
clearVideoView = Player::clearVideoTextureView,
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType")
|
||||
}
|
||||
@ -78,8 +78,8 @@ private fun <T : View> PlayerSurfaceInternal(
|
||||
player: Player,
|
||||
modifier: Modifier,
|
||||
createView: (Context) -> T,
|
||||
setViewOnPlayer: (Player, T) -> Unit,
|
||||
clearViewFromPlayer: (Player, T) -> Unit,
|
||||
setVideoView: Player.(T) -> Unit,
|
||||
clearVideoView: Player.(T) -> Unit,
|
||||
) {
|
||||
var view by remember { mutableStateOf<T?>(null) }
|
||||
var registeredPlayer by remember { mutableStateOf<Player?>(null) }
|
||||
@ -88,11 +88,11 @@ private fun <T : View> PlayerSurfaceInternal(
|
||||
LaunchedEffect(view, player) {
|
||||
registeredPlayer?.let { previousPlayer ->
|
||||
if (previousPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE))
|
||||
clearViewFromPlayer(previousPlayer, view)
|
||||
previousPlayer.clearVideoView(view)
|
||||
registeredPlayer = null
|
||||
}
|
||||
if (player.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) {
|
||||
setViewOnPlayer(player, view)
|
||||
player.setVideoView(view)
|
||||
registeredPlayer = player
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user