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