mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Compare commits
3 Commits
c8a3361cc8
...
4bfa154acd
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4bfa154acd | ||
![]() |
71fb3ad5a5 | ||
![]() |
eae886fe28 |
@ -76,6 +76,7 @@
|
|||||||
correspondingly.
|
correspondingly.
|
||||||
* Add `DownloadHelper.Factory` with which the static
|
* Add `DownloadHelper.Factory` with which the static
|
||||||
`DownloadHelper.forMediaItem()` methods are replaced.
|
`DownloadHelper.forMediaItem()` methods are replaced.
|
||||||
|
* Add `Factory` for `SegmentDownloader` implementations.
|
||||||
* OkHttp extension:
|
* OkHttp extension:
|
||||||
* Cronet extension:
|
* Cronet extension:
|
||||||
* RTMP extension:
|
* RTMP extension:
|
||||||
|
@ -39,17 +39,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Constructors accessed via reflection in DefaultDownloaderFactory
|
# Constructors accessed via reflection in DefaultDownloaderFactory
|
||||||
-dontnote androidx.media3.exoplayer.dash.offline.DashDownloader
|
-dontnote androidx.media3.exoplayer.dash.offline.DashDownloader$Factory
|
||||||
-keepclassmembers class androidx.media3.exoplayer.dash.offline.DashDownloader {
|
-keepclassmembers class androidx.media3.exoplayer.dash.offline.DashDownloader$Factory {
|
||||||
<init>(androidx.media3.common.MediaItem, androidx.media3.datasource.cache.CacheDataSource$Factory, java.util.concurrent.Executor);
|
<init>(androidx.media3.datasource.cache.CacheDataSource$Factory);
|
||||||
}
|
}
|
||||||
-dontnote androidx.media3.exoplayer.hls.offline.HlsDownloader
|
-dontnote androidx.media3.exoplayer.hls.offline.HlsDownloader$Factory
|
||||||
-keepclassmembers class androidx.media3.exoplayer.hls.offline.HlsDownloader {
|
-keepclassmembers class androidx.media3.exoplayer.hls.offline.HlsDownloader$Factory {
|
||||||
<init>(androidx.media3.common.MediaItem, androidx.media3.datasource.cache.CacheDataSource$Factory, java.util.concurrent.Executor);
|
<init>(androidx.media3.datasource.cache.CacheDataSource$Factory);
|
||||||
}
|
}
|
||||||
-dontnote androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader
|
-dontnote androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader$Factory
|
||||||
-keepclassmembers class androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader {
|
-keepclassmembers class androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader$Factory {
|
||||||
<init>(androidx.media3.common.MediaItem, androidx.media3.datasource.cache.CacheDataSource$Factory, java.util.concurrent.Executor);
|
<init>(androidx.media3.datasource.cache.CacheDataSource$Factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Constructors accessed via reflection in DefaultMediaSourceFactory
|
# Constructors accessed via reflection in DefaultMediaSourceFactory
|
||||||
|
@ -263,6 +263,7 @@ public interface AudioSink {
|
|||||||
* @param audioTrackState The underlying {@link AudioTrack}'s state.
|
* @param audioTrackState The underlying {@link AudioTrack}'s state.
|
||||||
* @param sampleRate The requested sample rate in Hz.
|
* @param sampleRate The requested sample rate in Hz.
|
||||||
* @param channelConfig The requested channel configuration.
|
* @param channelConfig The requested channel configuration.
|
||||||
|
* @param encoding The requested encoding.
|
||||||
* @param bufferSize The requested buffer size in bytes.
|
* @param bufferSize The requested buffer size in bytes.
|
||||||
* @param format The input format of the sink when the error occurs.
|
* @param format The input format of the sink when the error occurs.
|
||||||
* @param isRecoverable Whether the exception can be recovered by recreating the sink.
|
* @param isRecoverable Whether the exception can be recovered by recreating the sink.
|
||||||
@ -272,6 +273,7 @@ public interface AudioSink {
|
|||||||
int audioTrackState,
|
int audioTrackState,
|
||||||
int sampleRate,
|
int sampleRate,
|
||||||
int channelConfig,
|
int channelConfig,
|
||||||
|
int encoding,
|
||||||
int bufferSize,
|
int bufferSize,
|
||||||
Format format,
|
Format format,
|
||||||
boolean isRecoverable,
|
boolean isRecoverable,
|
||||||
@ -280,7 +282,15 @@ public interface AudioSink {
|
|||||||
"AudioTrack init failed "
|
"AudioTrack init failed "
|
||||||
+ audioTrackState
|
+ audioTrackState
|
||||||
+ " "
|
+ " "
|
||||||
+ ("Config(" + sampleRate + ", " + channelConfig + ", " + bufferSize + ")")
|
+ ("Config("
|
||||||
|
+ sampleRate
|
||||||
|
+ ", "
|
||||||
|
+ channelConfig
|
||||||
|
+ ", "
|
||||||
|
+ encoding
|
||||||
|
+ ", "
|
||||||
|
+ bufferSize
|
||||||
|
+ ")")
|
||||||
+ " "
|
+ " "
|
||||||
+ format
|
+ format
|
||||||
+ (isRecoverable ? " (recoverable)" : ""),
|
+ (isRecoverable ? " (recoverable)" : ""),
|
||||||
|
@ -1118,6 +1118,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
audioTrackConfig.sampleRate,
|
audioTrackConfig.sampleRate,
|
||||||
audioTrackConfig.channelConfig,
|
audioTrackConfig.channelConfig,
|
||||||
audioTrackConfig.encoding,
|
audioTrackConfig.encoding,
|
||||||
|
audioTrackConfig.bufferSize,
|
||||||
inputFormat,
|
inputFormat,
|
||||||
/* isRecoverable= */ audioTrackConfig.offload,
|
/* isRecoverable= */ audioTrackConfig.offload,
|
||||||
e);
|
e);
|
||||||
@ -1136,6 +1137,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
audioTrackConfig.sampleRate,
|
audioTrackConfig.sampleRate,
|
||||||
audioTrackConfig.channelConfig,
|
audioTrackConfig.channelConfig,
|
||||||
audioTrackConfig.encoding,
|
audioTrackConfig.encoding,
|
||||||
|
audioTrackConfig.bufferSize,
|
||||||
inputFormat,
|
inputFormat,
|
||||||
/* isRecoverable= */ audioTrackConfig.offload,
|
/* isRecoverable= */ audioTrackConfig.offload,
|
||||||
/* audioTrackException= */ null);
|
/* audioTrackException= */ null);
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.offline;
|
package androidx.media3.exoplayer.offline;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Util.contains;
|
||||||
|
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -23,7 +25,6 @@ import androidx.media3.common.util.Assertions;
|
|||||||
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.cache.CacheDataSource;
|
import androidx.media3.datasource.cache.CacheDataSource;
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,11 +35,9 @@ import java.util.concurrent.Executor;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class DefaultDownloaderFactory implements DownloaderFactory {
|
public class DefaultDownloaderFactory implements DownloaderFactory {
|
||||||
|
|
||||||
private static final SparseArray<Constructor<? extends Downloader>> CONSTRUCTORS =
|
|
||||||
createDownloaderConstructors();
|
|
||||||
|
|
||||||
private final CacheDataSource.Factory cacheDataSourceFactory;
|
private final CacheDataSource.Factory cacheDataSourceFactory;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
|
private final SparseArray<SegmentDownloaderFactory> segmentDownloaderFactories;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance.
|
||||||
@ -66,6 +65,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
|
|||||||
CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
||||||
this.cacheDataSourceFactory = Assertions.checkNotNull(cacheDataSourceFactory);
|
this.cacheDataSourceFactory = Assertions.checkNotNull(cacheDataSourceFactory);
|
||||||
this.executor = Assertions.checkNotNull(executor);
|
this.executor = Assertions.checkNotNull(executor);
|
||||||
|
this.segmentDownloaderFactories = new SparseArray<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -76,7 +76,7 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
|
|||||||
case C.CONTENT_TYPE_DASH:
|
case C.CONTENT_TYPE_DASH:
|
||||||
case C.CONTENT_TYPE_HLS:
|
case C.CONTENT_TYPE_HLS:
|
||||||
case C.CONTENT_TYPE_SS:
|
case C.CONTENT_TYPE_SS:
|
||||||
return createDownloader(request, contentType);
|
return createSegmentDownloader(request, contentType);
|
||||||
case C.CONTENT_TYPE_OTHER:
|
case C.CONTENT_TYPE_OTHER:
|
||||||
@Nullable DownloadRequest.ByteRange byteRange = request.byteRange;
|
@Nullable DownloadRequest.ByteRange byteRange = request.byteRange;
|
||||||
return new ProgressiveDownloader(
|
return new ProgressiveDownloader(
|
||||||
@ -93,64 +93,78 @@ public class DefaultDownloaderFactory implements DownloaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Downloader createDownloader(DownloadRequest request, @C.ContentType int contentType) {
|
private Downloader createSegmentDownloader(
|
||||||
@Nullable Constructor<? extends Downloader> constructor = CONSTRUCTORS.get(contentType);
|
DownloadRequest request, @C.ContentType int contentType) {
|
||||||
if (constructor == null) {
|
SegmentDownloaderFactory downloaderFactory =
|
||||||
throw new IllegalStateException("Module missing for content type " + contentType);
|
getSegmentDownloaderFactory(contentType, cacheDataSourceFactory);
|
||||||
}
|
|
||||||
MediaItem mediaItem =
|
MediaItem mediaItem =
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri(request.uri)
|
.setUri(request.uri)
|
||||||
.setStreamKeys(request.streamKeys)
|
.setStreamKeys(request.streamKeys)
|
||||||
.setCustomCacheKey(request.customCacheKey)
|
.setCustomCacheKey(request.customCacheKey)
|
||||||
.build();
|
.build();
|
||||||
try {
|
return downloaderFactory.setExecutor(executor).create(mediaItem);
|
||||||
return constructor.newInstance(mediaItem, cacheDataSourceFactory, executor);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Failed to instantiate downloader for content type " + contentType, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LINT.IfChange
|
// LINT.IfChange
|
||||||
private static SparseArray<Constructor<? extends Downloader>> createDownloaderConstructors() {
|
private SegmentDownloaderFactory getSegmentDownloaderFactory(
|
||||||
SparseArray<Constructor<? extends Downloader>> array = new SparseArray<>();
|
@C.ContentType int contentType, CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
try {
|
if (contains(segmentDownloaderFactories, contentType)) {
|
||||||
array.put(
|
return segmentDownloaderFactories.get(contentType);
|
||||||
C.CONTENT_TYPE_DASH,
|
|
||||||
getDownloaderConstructor(
|
|
||||||
Class.forName("androidx.media3.exoplayer.dash.offline.DashDownloader")));
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
// Expected if the app was built without the DASH module.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SegmentDownloaderFactory downloaderFactory;
|
||||||
try {
|
try {
|
||||||
array.put(
|
downloaderFactory = loadSegmentDownloaderFactory(contentType, cacheDataSourceFactory);
|
||||||
C.CONTENT_TYPE_HLS,
|
|
||||||
getDownloaderConstructor(
|
|
||||||
Class.forName("androidx.media3.exoplayer.hls.offline.HlsDownloader")));
|
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
// Expected if the app was built without the HLS module.
|
throw new IllegalStateException("Module missing for content type " + contentType, e);
|
||||||
}
|
}
|
||||||
try {
|
return downloaderFactory;
|
||||||
array.put(
|
|
||||||
C.CONTENT_TYPE_SS,
|
|
||||||
getDownloaderConstructor(
|
|
||||||
Class.forName("androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader")));
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
// Expected if the app was built without the SmoothStreaming module.
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Constructor<? extends Downloader> getDownloaderConstructor(Class<?> clazz) {
|
private SegmentDownloaderFactory loadSegmentDownloaderFactory(
|
||||||
|
@C.ContentType int contentType, CacheDataSource.Factory cacheDataSourceFactory)
|
||||||
|
throws ClassNotFoundException {
|
||||||
|
SegmentDownloaderFactory factory;
|
||||||
|
switch (contentType) {
|
||||||
|
case C.CONTENT_TYPE_DASH:
|
||||||
|
factory =
|
||||||
|
createSegmentDownloaderFactory(
|
||||||
|
Class.forName("androidx.media3.exoplayer.dash.offline.DashDownloader$Factory")
|
||||||
|
.asSubclass(SegmentDownloaderFactory.class),
|
||||||
|
cacheDataSourceFactory);
|
||||||
|
break;
|
||||||
|
case C.CONTENT_TYPE_HLS:
|
||||||
|
factory =
|
||||||
|
createSegmentDownloaderFactory(
|
||||||
|
Class.forName("androidx.media3.exoplayer.hls.offline.HlsDownloader$Factory")
|
||||||
|
.asSubclass(SegmentDownloaderFactory.class),
|
||||||
|
cacheDataSourceFactory);
|
||||||
|
break;
|
||||||
|
case C.CONTENT_TYPE_SS:
|
||||||
|
factory =
|
||||||
|
createSegmentDownloaderFactory(
|
||||||
|
Class.forName(
|
||||||
|
"androidx.media3.exoplayer.smoothstreaming.offline.SsDownloader$Factory")
|
||||||
|
.asSubclass(SegmentDownloaderFactory.class),
|
||||||
|
cacheDataSourceFactory);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported type: " + contentType);
|
||||||
|
}
|
||||||
|
segmentDownloaderFactories.put(contentType, factory);
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SegmentDownloaderFactory createSegmentDownloaderFactory(
|
||||||
|
Class<? extends SegmentDownloaderFactory> clazz,
|
||||||
|
CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
try {
|
try {
|
||||||
return clazz
|
return clazz
|
||||||
.asSubclass(Downloader.class)
|
.getConstructor(CacheDataSource.Factory.class)
|
||||||
.getConstructor(MediaItem.class, CacheDataSource.Factory.class, Executor.class);
|
.newInstance(cacheDataSourceFactory);
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (Exception e) {
|
||||||
// The downloader is present, but the expected constructor is missing.
|
throw new IllegalStateException("Downloader factory missing", e);
|
||||||
throw new IllegalStateException("Downloader constructor missing", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// LINT.ThenChange(../../../../../../../proguard-rules.txt)
|
// LINT.ThenChange(../../../../../../../proguard-rules.txt)
|
||||||
|
@ -38,6 +38,7 @@ import androidx.media3.datasource.cache.CacheWriter;
|
|||||||
import androidx.media3.datasource.cache.ContentMetadata;
|
import androidx.media3.datasource.cache.ContentMetadata;
|
||||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||||
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -56,6 +57,37 @@ import java.util.concurrent.Executor;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public abstract class SegmentDownloader<M extends FilterableManifest<M>> implements Downloader {
|
public abstract class SegmentDownloader<M extends FilterableManifest<M>> implements Downloader {
|
||||||
|
|
||||||
|
/** A base class of the factory of the concrete extension of {@link SegmentDownloader}. */
|
||||||
|
protected abstract static class BaseFactory<M extends FilterableManifest<M>>
|
||||||
|
implements SegmentDownloaderFactory {
|
||||||
|
|
||||||
|
protected final CacheDataSource.Factory cacheDataSourceFactory;
|
||||||
|
protected Parser<M> manifestParser;
|
||||||
|
protected Executor executor;
|
||||||
|
protected long maxMergedSegmentStartTimeDiffMs;
|
||||||
|
|
||||||
|
public BaseFactory(CacheDataSource.Factory cacheDataSourceFactory, Parser<M> manifestParser) {
|
||||||
|
this.cacheDataSourceFactory = cacheDataSourceFactory;
|
||||||
|
this.manifestParser = manifestParser;
|
||||||
|
this.executor = Runnable::run;
|
||||||
|
this.maxMergedSegmentStartTimeDiffMs = DEFAULT_MAX_MERGED_SEGMENT_START_TIME_DIFF_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public BaseFactory<M> setExecutor(Executor executor) {
|
||||||
|
this.executor = executor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public BaseFactory<M> setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) {
|
||||||
|
this.maxMergedSegmentStartTimeDiffMs = maxMergedSegmentStartTimeDiffMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Smallest unit of content to be downloaded. */
|
/** Smallest unit of content to be downloaded. */
|
||||||
protected static class Segment implements Comparable<Segment> {
|
protected static class Segment implements Comparable<Segment> {
|
||||||
|
|
||||||
@ -103,19 +135,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
|
|||||||
|
|
||||||
private volatile boolean isCanceled;
|
private volatile boolean isCanceled;
|
||||||
|
|
||||||
/**
|
protected SegmentDownloader(
|
||||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
|
||||||
* @param manifestParser A parser for manifests belonging to the media to be downloaded.
|
|
||||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
|
||||||
* download will be written.
|
|
||||||
* @param executor An {@link Executor} used to make requests for the media being downloaded.
|
|
||||||
* Providing an {@link Executor} that uses multiple threads will speed up the download by
|
|
||||||
* allowing parts of it to be executed in parallel.
|
|
||||||
* @param maxMergedSegmentStartTimeDiffMs The maximum difference of the start time of two
|
|
||||||
* segments, up to which the segments (of the same URI) should be merged into a single
|
|
||||||
* download segment, in milliseconds.
|
|
||||||
*/
|
|
||||||
public SegmentDownloader(
|
|
||||||
MediaItem mediaItem,
|
MediaItem mediaItem,
|
||||||
Parser<M> manifestParser,
|
Parser<M> manifestParser,
|
||||||
CacheDataSource.Factory cacheDataSourceFactory,
|
CacheDataSource.Factory cacheDataSourceFactory,
|
||||||
@ -414,12 +434,14 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("FutureReturnValueIgnored")
|
||||||
private void removeActiveRunnable(RunnableFutureTask<?, ?> runnable) {
|
private void removeActiveRunnable(RunnableFutureTask<?, ?> runnable) {
|
||||||
synchronized (activeRunnables) {
|
synchronized (activeRunnables) {
|
||||||
activeRunnables.remove(runnable);
|
activeRunnables.remove(runnable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("FutureReturnValueIgnored")
|
||||||
private void removeActiveRunnable(int index) {
|
private void removeActiveRunnable(int index) {
|
||||||
synchronized (activeRunnables) {
|
synchronized (activeRunnables) {
|
||||||
activeRunnables.remove(index);
|
activeRunnables.remove(index);
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.media3.exoplayer.offline;
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/** A factory to create {@linkplain SegmentDownloader segment downloaders}. */
|
||||||
|
@UnstableApi
|
||||||
|
/* package */ interface SegmentDownloaderFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Executor} used to make requests for the media being downloaded. Providing an
|
||||||
|
* {@link Executor} that uses multiple threads will speed up the download by allowing parts of it
|
||||||
|
* to be executed in parallel.
|
||||||
|
*/
|
||||||
|
SegmentDownloaderFactory setExecutor(Executor executor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum difference of the start time of two segments, up to which the segments (of the
|
||||||
|
* same URI) should be merged into a single download segment, in milliseconds.
|
||||||
|
*/
|
||||||
|
SegmentDownloaderFactory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the segment downloader.
|
||||||
|
*
|
||||||
|
* @param mediaItem The {@link MediaItem} to be downloaded.
|
||||||
|
*/
|
||||||
|
SegmentDownloader<?> create(MediaItem mediaItem);
|
||||||
|
}
|
@ -366,7 +366,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VideoSink getSink(int inputIndex) {
|
public VideoSink getSink(int inputIndex) {
|
||||||
checkState(!contains(inputVideoSinks, inputIndex));
|
if (contains(inputVideoSinks, inputIndex)) {
|
||||||
|
return inputVideoSinks.get(inputIndex);
|
||||||
|
}
|
||||||
InputVideoSink inputVideoSink = new InputVideoSink(context, inputIndex);
|
InputVideoSink inputVideoSink = new InputVideoSink(context, inputIndex);
|
||||||
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
||||||
addListener(inputVideoSink);
|
addListener(inputVideoSink);
|
||||||
@ -730,7 +732,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void redraw() {
|
public void redraw() {
|
||||||
checkState(isInitialized());
|
if (!isInitialized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Resignal EOS only for the last item.
|
// Resignal EOS only for the last item.
|
||||||
boolean needsResignalEndOfCurrentInputStream = signaledEndOfStream;
|
boolean needsResignalEndOfCurrentInputStream = signaledEndOfStream;
|
||||||
long replayedPresentationTimeUs = lastOutputBufferPresentationTimeUs;
|
long replayedPresentationTimeUs = lastOutputBufferPresentationTimeUs;
|
||||||
|
@ -334,6 +334,7 @@ public class EndToEndOffloadFailureRecoveryTest {
|
|||||||
AudioTrack.STATE_UNINITIALIZED,
|
AudioTrack.STATE_UNINITIALIZED,
|
||||||
/* sampleRate= */ 48_000,
|
/* sampleRate= */ 48_000,
|
||||||
/* channelConfig= */ 0,
|
/* channelConfig= */ 0,
|
||||||
|
/* encoding= */ Format.NO_VALUE,
|
||||||
/* bufferSize= */ C.LENGTH_UNSET,
|
/* bufferSize= */ C.LENGTH_UNSET,
|
||||||
inputFormat,
|
inputFormat,
|
||||||
/* isRecoverable= */ true,
|
/* isRecoverable= */ true,
|
||||||
|
@ -41,6 +41,7 @@ import androidx.media3.exoplayer.offline.SegmentDownloader;
|
|||||||
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
||||||
import androidx.media3.extractor.ChunkIndex;
|
import androidx.media3.extractor.ChunkIndex;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -60,12 +61,11 @@ import java.util.concurrent.Executor;
|
|||||||
* // Create a downloader for the first representation of the first adaptation set of the first
|
* // Create a downloader for the first representation of the first adaptation set of the first
|
||||||
* // period.
|
* // period.
|
||||||
* DashDownloader dashDownloader =
|
* DashDownloader dashDownloader =
|
||||||
* new DashDownloader(
|
* new DashDownloader.Factory(cacheDataSourceFactory)
|
||||||
* new MediaItem.Builder()
|
* .create(new MediaItem.Builder()
|
||||||
* .setUri(manifestUrl)
|
* .setUri(manifestUrl)
|
||||||
* .setStreamKeys(Collections.singletonList(new StreamKey(0, 0, 0)))
|
* .setStreamKeys(ImmutableList.of(new StreamKey(0, 0, 0)))
|
||||||
* .build(),
|
* .build());
|
||||||
* cacheDataSourceFactory);
|
|
||||||
* // Perform the download.
|
* // Perform the download.
|
||||||
* dashDownloader.download(progressListener);
|
* dashDownloader.download(progressListener);
|
||||||
* // Use the downloaded data for playback.
|
* // Use the downloaded data for playback.
|
||||||
@ -76,29 +76,81 @@ import java.util.concurrent.Executor;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class DashDownloader extends SegmentDownloader<DashManifest> {
|
public final class DashDownloader extends SegmentDownloader<DashManifest> {
|
||||||
|
|
||||||
private final BaseUrlExclusionList baseUrlExclusionList;
|
/** A factory for {@linkplain DashDownloader DASH downloaders}. */
|
||||||
|
public static final class Factory extends BaseFactory<DashManifest> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a factory for {@link DashDownloader}.
|
||||||
*
|
*
|
||||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
|
||||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
||||||
* download will be written.
|
* download will be written.
|
||||||
*/
|
*/
|
||||||
|
public Factory(CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
|
super(cacheDataSourceFactory, new DashManifestParser());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a parser for DASH manifests.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setManifestParser(DashManifestParser manifestParser) {
|
||||||
|
this.manifestParser = manifestParser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Executor} used to make requests for the media being downloaded. Providing an
|
||||||
|
* {@link Executor} that uses multiple threads will speed up the download by allowing parts of
|
||||||
|
* it to be executed in parallel.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setExecutor(Executor executor) {
|
||||||
|
return (Factory) super.setExecutor(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum difference of the start time of two segments, up to which the segments (of
|
||||||
|
* the same URI) should be merged into a single download segment, in milliseconds.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) {
|
||||||
|
return (Factory) super.setMaxMergedSegmentStartTimeDiffMs(maxMergedSegmentStartTimeDiffMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates {@linkplain DashDownloader DASH downloaders}. */
|
||||||
|
@Override
|
||||||
|
public DashDownloader create(MediaItem mediaItem) {
|
||||||
|
return new DashDownloader(
|
||||||
|
mediaItem,
|
||||||
|
manifestParser,
|
||||||
|
cacheDataSourceFactory,
|
||||||
|
executor,
|
||||||
|
maxMergedSegmentStartTimeDiffMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final BaseUrlExclusionList baseUrlExclusionList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link DashDownloader.Factory#create(MediaItem)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public DashDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
public DashDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
this(mediaItem, cacheDataSourceFactory, Runnable::run);
|
this(mediaItem, cacheDataSourceFactory, Runnable::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* @deprecated Use {@link DashDownloader.Factory#create(MediaItem)} instead.
|
||||||
*
|
|
||||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
|
||||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
|
||||||
* download will be written.
|
|
||||||
* @param executor An {@link Executor} used to make requests for the media being downloaded.
|
|
||||||
* Providing an {@link Executor} that uses multiple threads will speed up the download by
|
|
||||||
* allowing parts of it to be executed in parallel.
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public DashDownloader(
|
public DashDownloader(
|
||||||
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
||||||
this(
|
this(
|
||||||
@ -123,7 +175,7 @@ public final class DashDownloader extends SegmentDownloader<DashManifest> {
|
|||||||
* segments, up to which the segments (of the same URI) should be merged into a single
|
* segments, up to which the segments (of the same URI) should be merged into a single
|
||||||
* download segment, in milliseconds.
|
* download segment, in milliseconds.
|
||||||
*/
|
*/
|
||||||
public DashDownloader(
|
private DashDownloader(
|
||||||
MediaItem mediaItem,
|
MediaItem mediaItem,
|
||||||
Parser<DashManifest> manifestParser,
|
Parser<DashManifest> manifestParser,
|
||||||
CacheDataSource.Factory cacheDataSourceFactory,
|
CacheDataSource.Factory cacheDataSourceFactory,
|
||||||
|
@ -340,9 +340,8 @@ public class DashDownloaderTest {
|
|||||||
new CacheDataSource.Factory()
|
new CacheDataSource.Factory()
|
||||||
.setCache(cache)
|
.setCache(cache)
|
||||||
.setUpstreamDataSourceFactory(upstreamDataSourceFactory);
|
.setUpstreamDataSourceFactory(upstreamDataSourceFactory);
|
||||||
return new DashDownloader(
|
return new DashDownloader.Factory(cacheDataSourceFactory)
|
||||||
new MediaItem.Builder().setUri(TEST_MPD_URI).setStreamKeys(keysList(keys)).build(),
|
.create(new MediaItem.Builder().setUri(TEST_MPD_URI).setStreamKeys(keysList(keys)).build());
|
||||||
cacheDataSourceFactory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ArrayList<StreamKey> keysList(StreamKey... keys) {
|
private static ArrayList<StreamKey> keysList(StreamKey... keys) {
|
||||||
|
@ -29,6 +29,7 @@ import androidx.media3.exoplayer.hls.playlist.HlsPlaylist;
|
|||||||
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
|
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
|
||||||
import androidx.media3.exoplayer.offline.SegmentDownloader;
|
import androidx.media3.exoplayer.offline.SegmentDownloader;
|
||||||
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -48,14 +49,13 @@ import java.util.concurrent.Executor;
|
|||||||
* .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
|
* .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
|
||||||
* // Create a downloader for the first variant in a multivariant playlist.
|
* // Create a downloader for the first variant in a multivariant playlist.
|
||||||
* HlsDownloader hlsDownloader =
|
* HlsDownloader hlsDownloader =
|
||||||
* new HlsDownloader(
|
* new HlsDownloader.Factory(cacheDataSourceFactory)
|
||||||
* new MediaItem.Builder()
|
* .create(new MediaItem.Builder()
|
||||||
* .setUri(playlistUri)
|
* .setUri(playlistUri)
|
||||||
* .setStreamKeys(
|
* .setStreamKeys(
|
||||||
* Collections.singletonList(
|
* ImmutableList.of(
|
||||||
* new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, 0)))
|
* new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, 0)))
|
||||||
* .build(),
|
* .build());
|
||||||
* Collections.singletonList();
|
|
||||||
* // Perform the download.
|
* // Perform the download.
|
||||||
* hlsDownloader.download(progressListener);
|
* hlsDownloader.download(progressListener);
|
||||||
* // Use the downloaded data for playback.
|
* // Use the downloaded data for playback.
|
||||||
@ -66,27 +66,79 @@ import java.util.concurrent.Executor;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
|
public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
|
||||||
|
|
||||||
|
/** A factory for {@linkplain HlsDownloader HLS downloaders}. */
|
||||||
|
public static final class Factory extends BaseFactory<HlsPlaylist> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a factory for {@link HlsDownloader}.
|
||||||
*
|
*
|
||||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
|
||||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
||||||
* download will be written.
|
* download will be written.
|
||||||
*/
|
*/
|
||||||
|
public Factory(CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
|
super(cacheDataSourceFactory, new HlsPlaylistParser());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a parser for HLS playlists.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setManifestParser(HlsPlaylistParser manifestParser) {
|
||||||
|
this.manifestParser = manifestParser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Executor} used to make requests for the media being downloaded. Providing an
|
||||||
|
* {@link Executor} that uses multiple threads will speed up the download by allowing parts of
|
||||||
|
* it to be executed in parallel.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setExecutor(Executor executor) {
|
||||||
|
return (Factory) super.setExecutor(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum difference of the start time of two segments, up to which the segments (of
|
||||||
|
* the same URI) should be merged into a single download segment, in milliseconds.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) {
|
||||||
|
return (Factory) super.setMaxMergedSegmentStartTimeDiffMs(maxMergedSegmentStartTimeDiffMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates {@linkplain HlsDownloader HLS downloaders}. */
|
||||||
|
@Override
|
||||||
|
public HlsDownloader create(MediaItem mediaItem) {
|
||||||
|
return new HlsDownloader(
|
||||||
|
mediaItem,
|
||||||
|
manifestParser,
|
||||||
|
cacheDataSourceFactory,
|
||||||
|
executor,
|
||||||
|
maxMergedSegmentStartTimeDiffMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link HlsDownloader.Factory#create(MediaItem)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public HlsDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
public HlsDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
this(mediaItem, cacheDataSourceFactory, Runnable::run);
|
this(mediaItem, cacheDataSourceFactory, Runnable::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* @deprecated Use {@link HlsDownloader.Factory#create(MediaItem)} instead.
|
||||||
*
|
|
||||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
|
||||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
|
||||||
* download will be written.
|
|
||||||
* @param executor An {@link Executor} used to make requests for the media being downloaded.
|
|
||||||
* Providing an {@link Executor} that uses multiple threads will speed up the download by
|
|
||||||
* allowing parts of it to be executed in parallel.
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public HlsDownloader(
|
public HlsDownloader(
|
||||||
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
||||||
this(
|
this(
|
||||||
@ -111,7 +163,7 @@ public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
|
|||||||
* segments, up to which the segments (of the same URI) should be merged into a single
|
* segments, up to which the segments (of the same URI) should be merged into a single
|
||||||
* download segment, in milliseconds.
|
* download segment, in milliseconds.
|
||||||
*/
|
*/
|
||||||
public HlsDownloader(
|
private HlsDownloader(
|
||||||
MediaItem mediaItem,
|
MediaItem mediaItem,
|
||||||
Parser<HlsPlaylist> manifestParser,
|
Parser<HlsPlaylist> manifestParser,
|
||||||
CacheDataSource.Factory cacheDataSourceFactory,
|
CacheDataSource.Factory cacheDataSourceFactory,
|
||||||
|
@ -223,9 +223,8 @@ public class HlsDownloaderTest {
|
|||||||
new CacheDataSource.Factory()
|
new CacheDataSource.Factory()
|
||||||
.setCache(cache)
|
.setCache(cache)
|
||||||
.setUpstreamDataSourceFactory(new FakeDataSource.Factory().setFakeDataSet(fakeDataSet));
|
.setUpstreamDataSourceFactory(new FakeDataSource.Factory().setFakeDataSet(fakeDataSet));
|
||||||
return new HlsDownloader(
|
return new HlsDownloader.Factory(cacheDataSourceFactory)
|
||||||
new MediaItem.Builder().setUri(mediaPlaylistUri).setStreamKeys(keys).build(),
|
.create(new MediaItem.Builder().setUri(mediaPlaylistUri).setStreamKeys(keys).build());
|
||||||
cacheDataSourceFactory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ArrayList<StreamKey> getKeys(int... variantIndices) {
|
private static ArrayList<StreamKey> getKeys(int... variantIndices) {
|
||||||
|
@ -28,6 +28,7 @@ import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest;
|
|||||||
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest.StreamElement;
|
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest.StreamElement;
|
||||||
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifestParser;
|
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifestParser;
|
||||||
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@ -45,12 +46,11 @@ import java.util.concurrent.Executor;
|
|||||||
* .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
|
* .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
|
||||||
* // Create a downloader for the first track of the first stream element.
|
* // Create a downloader for the first track of the first stream element.
|
||||||
* SsDownloader ssDownloader =
|
* SsDownloader ssDownloader =
|
||||||
* new SsDownloader(
|
* new SsDownloader.Factory(cacheDataSourceFactory)
|
||||||
* new MediaItem.Builder()
|
* .create(new MediaItem.Builder()
|
||||||
* .setUri(manifestUri)
|
* .setUri(manifestUri)
|
||||||
* .setStreamKeys(Collections.singletonList(new StreamKey(0, 0)))
|
* .setStreamKeys(ImmutableList.of(new StreamKey(0, 0)))
|
||||||
* .build(),
|
* .build());
|
||||||
* cacheDataSourceFactory);
|
|
||||||
* // Perform the download.
|
* // Perform the download.
|
||||||
* ssDownloader.download(progressListener);
|
* ssDownloader.download(progressListener);
|
||||||
* // Use the downloaded data for playback.
|
* // Use the downloaded data for playback.
|
||||||
@ -61,27 +61,84 @@ import java.util.concurrent.Executor;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class SsDownloader extends SegmentDownloader<SsManifest> {
|
public final class SsDownloader extends SegmentDownloader<SsManifest> {
|
||||||
|
|
||||||
|
/** A factory for {@linkplain SsDownloader SmoothStreaming downloaders}. */
|
||||||
|
public static final class Factory extends BaseFactory<SsManifest> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates a factory for {@link SsDownloader}.
|
||||||
*
|
*
|
||||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
|
||||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
||||||
* download will be written.
|
* download will be written.
|
||||||
*/
|
*/
|
||||||
|
public Factory(CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
|
super(cacheDataSourceFactory, new SsManifestParser());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a parser for SmoothStreaming manifests.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setManifestParser(SsManifestParser manifestParser) {
|
||||||
|
this.manifestParser = manifestParser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Executor} used to make requests for the media being downloaded. Providing an
|
||||||
|
* {@link Executor} that uses multiple threads will speed up the download by allowing parts of
|
||||||
|
* it to be executed in parallel.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setExecutor(Executor executor) {
|
||||||
|
return (Factory) super.setExecutor(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum difference of the start time of two segments, up to which the segments (of
|
||||||
|
* the same URI) should be merged into a single download segment, in milliseconds.
|
||||||
|
*
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Factory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) {
|
||||||
|
return (Factory) super.setMaxMergedSegmentStartTimeDiffMs(maxMergedSegmentStartTimeDiffMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates {@linkplain SsDownloader SmoothStreaming downloaders}. */
|
||||||
|
@Override
|
||||||
|
public SsDownloader create(MediaItem mediaItem) {
|
||||||
|
return new SsDownloader(
|
||||||
|
mediaItem
|
||||||
|
.buildUpon()
|
||||||
|
.setUri(
|
||||||
|
Util.fixSmoothStreamingIsmManifestUri(
|
||||||
|
checkNotNull(mediaItem.localConfiguration).uri))
|
||||||
|
.build(),
|
||||||
|
manifestParser,
|
||||||
|
cacheDataSourceFactory,
|
||||||
|
executor,
|
||||||
|
maxMergedSegmentStartTimeDiffMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link SsDownloader.Factory#create(MediaItem)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public SsDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
public SsDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
||||||
this(mediaItem, cacheDataSourceFactory, Runnable::run);
|
this(mediaItem, cacheDataSourceFactory, Runnable::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* @deprecated Use {@link SsDownloader.Factory#create(MediaItem)} instead.
|
||||||
*
|
|
||||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
|
||||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
|
||||||
* download will be written.
|
|
||||||
* @param executor An {@link Executor} used to make requests for the media being downloaded.
|
|
||||||
* Providing an {@link Executor} that uses multiple threads will speed up the download by
|
|
||||||
* allowing parts of it to be executed in parallel.
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public SsDownloader(
|
public SsDownloader(
|
||||||
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
||||||
this(
|
this(
|
||||||
@ -111,7 +168,7 @@ public final class SsDownloader extends SegmentDownloader<SsManifest> {
|
|||||||
* segments, up to which the segments (of the same URI) should be merged into a single
|
* segments, up to which the segments (of the same URI) should be merged into a single
|
||||||
* download segment, in milliseconds.
|
* download segment, in milliseconds.
|
||||||
*/
|
*/
|
||||||
public SsDownloader(
|
private SsDownloader(
|
||||||
MediaItem mediaItem,
|
MediaItem mediaItem,
|
||||||
Parser<SsManifest> manifestParser,
|
Parser<SsManifest> manifestParser,
|
||||||
CacheDataSource.Factory cacheDataSourceFactory,
|
CacheDataSource.Factory cacheDataSourceFactory,
|
||||||
|
@ -126,8 +126,7 @@ public final class DashDownloadTest {
|
|||||||
new CacheDataSource.Factory()
|
new CacheDataSource.Factory()
|
||||||
.setCache(cache)
|
.setCache(cache)
|
||||||
.setUpstreamDataSourceFactory(httpDataSourceFactory);
|
.setUpstreamDataSourceFactory(httpDataSourceFactory);
|
||||||
return new DashDownloader(
|
return new DashDownloader.Factory(cacheDataSourceFactory)
|
||||||
new MediaItem.Builder().setUri(MANIFEST_URI).setStreamKeys(keys).build(),
|
.create(new MediaItem.Builder().setUri(MANIFEST_URI).setStreamKeys(keys).build());
|
||||||
cacheDataSourceFactory);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -481,6 +481,7 @@ public final class AndroidTestUtil {
|
|||||||
.setFrameRate(30.00f)
|
.setFrameRate(30.00f)
|
||||||
.setCodecs("avc1.42C033")
|
.setCodecs("avc1.42C033")
|
||||||
.build())
|
.build())
|
||||||
|
.setVideoDurationUs(1_000_000L)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public static final AssetInfo MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS =
|
public static final AssetInfo MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS =
|
||||||
|
@ -32,9 +32,11 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.VideoFrameProcessor;
|
import androidx.media3.common.VideoFrameProcessor;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.effect.Contrast;
|
import androidx.media3.effect.Contrast;
|
||||||
|
import androidx.media3.effect.GlEffect;
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.exoplayer.Renderer;
|
import androidx.media3.exoplayer.Renderer;
|
||||||
@ -43,6 +45,12 @@ import androidx.media3.exoplayer.util.EventLogger;
|
|||||||
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||||
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||||
import androidx.media3.transformer.AndroidTestUtil.ReplayVideoRenderer;
|
import androidx.media3.transformer.AndroidTestUtil.ReplayVideoRenderer;
|
||||||
|
import androidx.media3.transformer.Composition;
|
||||||
|
import androidx.media3.transformer.CompositionPlayer;
|
||||||
|
import androidx.media3.transformer.EditedMediaItem;
|
||||||
|
import androidx.media3.transformer.EditedMediaItemSequence;
|
||||||
|
import androidx.media3.transformer.Effects;
|
||||||
|
import androidx.media3.transformer.InputTimestampRecordingShaderProgram;
|
||||||
import androidx.media3.transformer.PlayerTestListener;
|
import androidx.media3.transformer.PlayerTestListener;
|
||||||
import androidx.media3.transformer.SurfaceTestActivity;
|
import androidx.media3.transformer.SurfaceTestActivity;
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||||
@ -64,8 +72,11 @@ public class ReplayCacheTest {
|
|||||||
private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000;
|
private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000;
|
||||||
|
|
||||||
private static final MediaItem VIDEO_MEDIA_ITEM_1 = MediaItem.fromUri(MP4_ASSET.uri);
|
private static final MediaItem VIDEO_MEDIA_ITEM_1 = MediaItem.fromUri(MP4_ASSET.uri);
|
||||||
|
private static final long VIDEO_MEDIA_ITEM_1_DURATION_US = MP4_ASSET.videoDurationUs;
|
||||||
private static final MediaItem VIDEO_MEDIA_ITEM_2 =
|
private static final MediaItem VIDEO_MEDIA_ITEM_2 =
|
||||||
MediaItem.fromUri(MP4_ASSET_WITH_INCREASING_TIMESTAMPS.uri);
|
MediaItem.fromUri(MP4_ASSET_WITH_INCREASING_TIMESTAMPS.uri);
|
||||||
|
private static final long VIDEO_MEDIA_ITEM_2_DURATION_US =
|
||||||
|
MP4_ASSET_WITH_INCREASING_TIMESTAMPS.videoDurationUs;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public ActivityScenarioRule<SurfaceTestActivity> rule =
|
public ActivityScenarioRule<SurfaceTestActivity> rule =
|
||||||
@ -74,6 +85,7 @@ public class ReplayCacheTest {
|
|||||||
private final Context context = getInstrumentation().getContext().getApplicationContext();
|
private final Context context = getInstrumentation().getContext().getApplicationContext();
|
||||||
private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||||
|
|
||||||
|
private CompositionPlayer compositionPlayer;
|
||||||
private ExoPlayer exoPlayer;
|
private ExoPlayer exoPlayer;
|
||||||
private SurfaceView surfaceView;
|
private SurfaceView surfaceView;
|
||||||
|
|
||||||
@ -88,6 +100,9 @@ public class ReplayCacheTest {
|
|||||||
getInstrumentation()
|
getInstrumentation()
|
||||||
.runOnMainSync(
|
.runOnMainSync(
|
||||||
() -> {
|
() -> {
|
||||||
|
if (compositionPlayer != null) {
|
||||||
|
compositionPlayer.release();
|
||||||
|
}
|
||||||
if (exoPlayer != null) {
|
if (exoPlayer != null) {
|
||||||
exoPlayer.release();
|
exoPlayer.release();
|
||||||
}
|
}
|
||||||
@ -159,4 +174,108 @@ public class ReplayCacheTest {
|
|||||||
// we don't currently replay (the frame at media item transition).
|
// we don't currently replay (the frame at media item transition).
|
||||||
assertThat(playedFrameTimestampsUs).hasSize(119);
|
assertThat(playedFrameTimestampsUs).hasSize(119);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void enableReplay_withCompositionPlayerSingleSequence_playsSequence() throws Exception {
|
||||||
|
assumeTrue(
|
||||||
|
"The MediaCodec decoder's output surface is sometimes dropping frames on emulator despite"
|
||||||
|
+ " using MediaFormat.KEY_ALLOW_FRAME_DROP.",
|
||||||
|
!Util.isRunningOnEmulator());
|
||||||
|
PlayerTestListener playerTestListener = new PlayerTestListener(TEST_TIMEOUT_MS * 1000);
|
||||||
|
InputTimestampRecordingShaderProgram inputTimestampRecordingShaderProgram =
|
||||||
|
new InputTimestampRecordingShaderProgram();
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
compositionPlayer =
|
||||||
|
new CompositionPlayer.Builder(context)
|
||||||
|
.experimentalSetEnableReplayableCache(true)
|
||||||
|
.build();
|
||||||
|
compositionPlayer.setVideoSurfaceView(surfaceView);
|
||||||
|
compositionPlayer.addListener(playerTestListener);
|
||||||
|
compositionPlayer.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onRenderedFirstFrame() {
|
||||||
|
compositionPlayer.experimentalRedrawLastFrame();
|
||||||
|
compositionPlayer.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
compositionPlayer.setComposition(
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_1)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_1_DURATION_US)
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_2)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_2_DURATION_US)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
new Contrast(0.5f),
|
||||||
|
(GlEffect)
|
||||||
|
(context, useHdr) -> inputTimestampRecordingShaderProgram)))
|
||||||
|
.build());
|
||||||
|
compositionPlayer.prepare();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerTestListener.waitUntilPlayerEnded();
|
||||||
|
|
||||||
|
int countOfFirstFrameRendered = 0;
|
||||||
|
for (long timestampUs : inputTimestampRecordingShaderProgram.getInputTimestampsUs()) {
|
||||||
|
if (timestampUs == 0) {
|
||||||
|
countOfFirstFrameRendered++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(countOfFirstFrameRendered).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rapidReplay_withCompositionPlayerSingleSequence_playsSequence() throws Exception {
|
||||||
|
assumeTrue(
|
||||||
|
"The MediaCodec decoder's output surface is sometimes dropping frames on emulator despite"
|
||||||
|
+ " using MediaFormat.KEY_ALLOW_FRAME_DROP.",
|
||||||
|
!Util.isRunningOnEmulator());
|
||||||
|
PlayerTestListener playerTestListener = new PlayerTestListener(TEST_TIMEOUT_MS);
|
||||||
|
Handler mainHandler = new Handler(instrumentation.getTargetContext().getMainLooper());
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
compositionPlayer =
|
||||||
|
new CompositionPlayer.Builder(context)
|
||||||
|
.experimentalSetEnableReplayableCache(true)
|
||||||
|
.build();
|
||||||
|
compositionPlayer.setVideoSurfaceView(surfaceView);
|
||||||
|
compositionPlayer.addListener(playerTestListener);
|
||||||
|
compositionPlayer.setComposition(
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_1)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_1_DURATION_US)
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_2)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_2_DURATION_US)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(new Contrast(0.5f))))
|
||||||
|
.build());
|
||||||
|
compositionPlayer.prepare();
|
||||||
|
compositionPlayer.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerTestListener.waitUntilPlayerReady();
|
||||||
|
for (int i = 0; i < 180; i++) {
|
||||||
|
// Replaying every 10 ms.
|
||||||
|
mainHandler.postDelayed(
|
||||||
|
compositionPlayer::experimentalRedrawLastFrame, /* delayMillis= */ 10 * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
playerTestListener.waitUntilPlayerEnded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ import androidx.media3.common.util.Log;
|
|||||||
import androidx.media3.common.util.Size;
|
import androidx.media3.common.util.Size;
|
||||||
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.effect.DefaultVideoFrameProcessor;
|
||||||
import androidx.media3.effect.SingleInputVideoGraph;
|
import androidx.media3.effect.SingleInputVideoGraph;
|
||||||
import androidx.media3.effect.TimestampAdjustment;
|
import androidx.media3.effect.TimestampAdjustment;
|
||||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||||
@ -125,6 +126,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
private boolean videoPrewarmingEnabled;
|
private boolean videoPrewarmingEnabled;
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
private VideoGraph.@MonotonicNonNull Factory videoGraphFactory;
|
private VideoGraph.@MonotonicNonNull Factory videoGraphFactory;
|
||||||
|
private boolean enableReplayableCache;
|
||||||
private boolean built;
|
private boolean built;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -246,6 +248,21 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether to enable replayable cache.
|
||||||
|
*
|
||||||
|
* <p>By default, the replayable cache is not enabled. Enable it to achieve accurate effect
|
||||||
|
* update, at the cost of using more power and computing resources.
|
||||||
|
*
|
||||||
|
* @param enableReplayableCache Whether replayable cache is enabled.
|
||||||
|
* @return This builder, for convenience.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder experimentalSetEnableReplayableCache(boolean enableReplayableCache) {
|
||||||
|
this.enableReplayableCache = enableReplayableCache;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the {@link CompositionPlayer} instance. Must be called at most once.
|
* Builds the {@link CompositionPlayer} instance. Must be called at most once.
|
||||||
*
|
*
|
||||||
@ -262,7 +279,11 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
audioSink = new DefaultAudioSink.Builder(context).build();
|
audioSink = new DefaultAudioSink.Builder(context).build();
|
||||||
}
|
}
|
||||||
if (videoGraphFactory == null) {
|
if (videoGraphFactory == null) {
|
||||||
videoGraphFactory = new SingleInputVideoGraph.Factory();
|
videoGraphFactory =
|
||||||
|
new SingleInputVideoGraph.Factory(
|
||||||
|
new DefaultVideoFrameProcessor.Factory.Builder()
|
||||||
|
.setEnableReplayableCache(enableReplayableCache)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
CompositionPlayer compositionPlayer = new CompositionPlayer(this);
|
CompositionPlayer compositionPlayer = new CompositionPlayer(this);
|
||||||
built = true;
|
built = true;
|
||||||
@ -311,11 +332,13 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
private final VideoGraph.Factory videoGraphFactory;
|
private final VideoGraph.Factory videoGraphFactory;
|
||||||
private final boolean videoPrewarmingEnabled;
|
private final boolean videoPrewarmingEnabled;
|
||||||
private final HandlerWrapper compositionInternalListenerHandler;
|
private final HandlerWrapper compositionInternalListenerHandler;
|
||||||
|
private final boolean enableReplayableCache;
|
||||||
|
|
||||||
/** Maps from input index to whether the video track is selected in that sequence. */
|
/** Maps from input index to whether the video track is selected in that sequence. */
|
||||||
private final SparseBooleanArray videoTracksSelected;
|
private final SparseBooleanArray videoTracksSelected;
|
||||||
|
|
||||||
private @MonotonicNonNull HandlerThread playbackThread;
|
private @MonotonicNonNull HandlerThread playbackThread;
|
||||||
|
private @MonotonicNonNull HandlerWrapper playbackThreadHandler;
|
||||||
private @MonotonicNonNull CompositionPlayerInternal compositionPlayerInternal;
|
private @MonotonicNonNull CompositionPlayerInternal compositionPlayerInternal;
|
||||||
private @MonotonicNonNull ImmutableList<MediaItemData> playlist;
|
private @MonotonicNonNull ImmutableList<MediaItemData> playlist;
|
||||||
private @MonotonicNonNull Composition composition;
|
private @MonotonicNonNull Composition composition;
|
||||||
@ -352,6 +375,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
videoGraphFactory = checkNotNull(builder.videoGraphFactory);
|
videoGraphFactory = checkNotNull(builder.videoGraphFactory);
|
||||||
videoPrewarmingEnabled = builder.videoPrewarmingEnabled;
|
videoPrewarmingEnabled = builder.videoPrewarmingEnabled;
|
||||||
compositionInternalListenerHandler = clock.createHandler(builder.looper, /* callback= */ null);
|
compositionInternalListenerHandler = clock.createHandler(builder.looper, /* callback= */ null);
|
||||||
|
this.enableReplayableCache = builder.enableReplayableCache;
|
||||||
videoTracksSelected = new SparseBooleanArray();
|
videoTracksSelected = new SparseBooleanArray();
|
||||||
players = new ArrayList<>();
|
players = new ArrayList<>();
|
||||||
compositionDurationUs = C.TIME_UNSET;
|
compositionDurationUs = C.TIME_UNSET;
|
||||||
@ -398,6 +422,21 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
this.composition = composition;
|
this.composition = composition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces the effect pipeline to redraw the effects immediately.
|
||||||
|
*
|
||||||
|
* <p>The player must be {@linkplain Builder#experimentalSetEnableReplayableCache built with
|
||||||
|
* replayable cache support}.
|
||||||
|
*/
|
||||||
|
public void experimentalRedrawLastFrame() {
|
||||||
|
checkState(enableReplayableCache);
|
||||||
|
if (playbackThreadHandler == null || playbackVideoGraphWrapper == null) {
|
||||||
|
// Ignore replays before setting a composition.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playbackThreadHandler.post(() -> checkNotNull(playbackVideoGraphWrapper).getSink(0).redraw());
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets the {@link Surface} and {@link Size} to render to. */
|
/** Sets the {@link Surface} and {@link Size} to render to. */
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void setVideoSurface(Surface surface, Size videoOutputSize) {
|
public void setVideoSurface(Surface surface, Size videoOutputSize) {
|
||||||
@ -714,6 +753,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
compositionDurationUs = getCompositionDurationUs(composition);
|
compositionDurationUs = getCompositionDurationUs(composition);
|
||||||
playbackThread = new HandlerThread("CompositionPlaybackThread", Process.THREAD_PRIORITY_AUDIO);
|
playbackThread = new HandlerThread("CompositionPlaybackThread", Process.THREAD_PRIORITY_AUDIO);
|
||||||
playbackThread.start();
|
playbackThread.start();
|
||||||
|
playbackThreadHandler = clock.createHandler(playbackThread.getLooper(), /* callback= */ null);
|
||||||
// Create the audio and video composition components now in order to setup the audio and video
|
// Create the audio and video composition components now in order to setup the audio and video
|
||||||
// pipelines. Once this method returns, further access to the audio and video graph wrappers
|
// pipelines. Once this method returns, further access to the audio and video graph wrappers
|
||||||
// must done on the playback thread only, to ensure related components are accessed from one
|
// must done on the playback thread only, to ensure related components are accessed from one
|
||||||
@ -734,6 +774,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
.setClock(clock)
|
.setClock(clock)
|
||||||
.setRequestOpenGlToneMapping(
|
.setRequestOpenGlToneMapping(
|
||||||
composition.hdrMode == Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
|
composition.hdrMode == Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
|
||||||
|
.setEnableReplayableCache(enableReplayableCache)
|
||||||
.build();
|
.build();
|
||||||
playbackVideoGraphWrapper.addListener(this);
|
playbackVideoGraphWrapper.addListener(this);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user