Allow Injection of custom Executor in ProgressiveMediaSource

This commit is contained in:
Colin Kho 2024-09-24 16:09:35 -07:00 committed by tonihei
parent ea837e494b
commit 10bb2e1501
3 changed files with 74 additions and 7 deletions

View File

@ -70,6 +70,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -172,6 +173,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
* @param singleSampleDurationUs The duration of media with a single sample in microseconds. * @param singleSampleDurationUs The duration of media with a single sample in microseconds.
* @param downloadExecutor An {@link Executor} for supplying the loader's thread.
*/ */
// maybeFinishPrepare is not posted to the handler until initialization completes. // maybeFinishPrepare is not posted to the handler until initialization completes.
@SuppressWarnings({"nullness:argument", "nullness:methodref.receiver.bound"}) @SuppressWarnings({"nullness:argument", "nullness:methodref.receiver.bound"})
@ -187,7 +189,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Allocator allocator, Allocator allocator,
@Nullable String customCacheKey, @Nullable String customCacheKey,
int continueLoadingCheckIntervalBytes, int continueLoadingCheckIntervalBytes,
long singleSampleDurationUs) { long singleSampleDurationUs,
@Nullable Executor downloadExecutor) {
this.uri = uri; this.uri = uri;
this.dataSource = dataSource; this.dataSource = dataSource;
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
@ -198,7 +201,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.allocator = allocator; this.allocator = allocator;
this.customCacheKey = customCacheKey; this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("ProgressiveMediaPeriod"); loader = downloadExecutor != null ?
new Loader(downloadExecutor) : new Loader("ProgressiveMediaPeriod");
this.progressiveMediaExtractor = progressiveMediaExtractor; this.progressiveMediaExtractor = progressiveMediaExtractor;
this.singleSampleDurationUs = singleSampleDurationUs; this.singleSampleDurationUs = singleSampleDurationUs;
loadCondition = new ConditionVariable(); loadCondition = new ConditionVariable();

View File

@ -37,7 +37,10 @@ import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.extractor.DefaultExtractorsFactory; import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.Extractor; import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.ExtractorsFactory;
import com.google.common.base.Supplier;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Optional;
import java.util.concurrent.Executor;
/** /**
* Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.
@ -64,6 +67,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
private DrmSessionManagerProvider drmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private int continueLoadingCheckIntervalBytes; private int continueLoadingCheckIntervalBytes;
@Nullable private Supplier<Executor> downloadExecutor;
/** /**
* Creates a new factory for {@link ProgressiveMediaSource}s. * Creates a new factory for {@link ProgressiveMediaSource}s.
@ -154,6 +158,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
this.drmSessionManagerProvider = drmSessionManagerProvider; this.drmSessionManagerProvider = drmSessionManagerProvider;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
this.downloadExecutor = () -> null;
} }
@CanIgnoreReturnValue @CanIgnoreReturnValue
@ -197,6 +202,20 @@ public final class ProgressiveMediaSource extends BaseMediaSource
return this; return this;
} }
/**
* Sets a supplier that can return an {@link Executor} that is used for loading the media. This
* is useful if the loading thread needs to be externally managed.
*
* @param downloadExecutor a {@link Supplier<Executor>} that provides an externally managed
* {@link Executor} for downloading and extraction.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
public Factory setDownloadExecutor(Supplier<Executor> downloadExecutor) {
this.downloadExecutor = downloadExecutor;
return this;
}
/** /**
* Returns a new {@link ProgressiveMediaSource} using the current parameters. * Returns a new {@link ProgressiveMediaSource} using the current parameters.
* *
@ -213,7 +232,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
progressiveMediaExtractorFactory, progressiveMediaExtractorFactory,
drmSessionManagerProvider.get(mediaItem), drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes); continueLoadingCheckIntervalBytes,
downloadExecutor);
} }
@Override @Override
@ -233,12 +253,12 @@ public final class ProgressiveMediaSource extends BaseMediaSource
private final DrmSessionManager drmSessionManager; private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
private final int continueLoadingCheckIntervalBytes; private final int continueLoadingCheckIntervalBytes;
@Nullable private final Supplier<Executor> downloadExecutor;
private boolean timelineIsPlaceholder; private boolean timelineIsPlaceholder;
private long timelineDurationUs; private long timelineDurationUs;
private boolean timelineIsSeekable; private boolean timelineIsSeekable;
private boolean timelineIsLive; private boolean timelineIsLive;
@Nullable private TransferListener transferListener; @Nullable private TransferListener transferListener;
@GuardedBy("this") @GuardedBy("this")
private MediaItem mediaItem; private MediaItem mediaItem;
@ -248,7 +268,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory, ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
int continueLoadingCheckIntervalBytes) { int continueLoadingCheckIntervalBytes,
@Nullable Supplier<Executor> downloadExecutor) {
this.mediaItem = mediaItem; this.mediaItem = mediaItem;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory; this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
@ -257,6 +278,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
this.timelineIsPlaceholder = true; this.timelineIsPlaceholder = true;
this.timelineDurationUs = C.TIME_UNSET; this.timelineDurationUs = C.TIME_UNSET;
this.downloadExecutor = downloadExecutor;
} }
@Override @Override
@ -312,7 +334,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
allocator, allocator,
localConfiguration.customCacheKey, localConfiguration.customCacheKey,
continueLoadingCheckIntervalBytes, continueLoadingCheckIntervalBytes,
Util.msToUs(localConfiguration.imageDurationMs)); Util.msToUs(localConfiguration.imageDurationMs),
downloadExecutor != null ? downloadExecutor.get() : null);
} }
@Override @Override

View File

@ -19,6 +19,7 @@ import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLoop
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.datasource.AssetDataSource; import androidx.media3.datasource.AssetDataSource;
import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.LoadingInfo;
@ -34,6 +35,8 @@ import androidx.media3.extractor.mp4.Mp4Extractor;
import androidx.media3.extractor.png.PngExtractor; import androidx.media3.extractor.png.PngExtractor;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test; import org.junit.Test;
@ -67,8 +70,29 @@ public final class ProgressiveMediaPeriodTest {
testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(extractor, C.TIME_UNSET); testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(extractor, C.TIME_UNSET);
} }
@Test
public void supplyingCustomDownloadExecutor_downloadsOnCustomThread() throws TimeoutException {
AtomicBoolean hasThreadRunBefore = new AtomicBoolean(false);
Executor executor =
Executors.newSingleThreadExecutor(
(r) -> new ExecutionTrackingThread(r, hasThreadRunBefore));
testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
new BundledExtractorsAdapter(Mp4Extractor.FACTORY), C.TIME_UNSET, executor);
assertThat(hasThreadRunBefore.get()).isTrue();
}
private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
ProgressiveMediaExtractor extractor, long imageDurationUs) throws TimeoutException { ProgressiveMediaExtractor extractor, long imageDurationUs) throws TimeoutException {
testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
extractor, imageDurationUs, null);
}
private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
ProgressiveMediaExtractor extractor,
long imageDurationUs,
@Nullable Executor executor) throws TimeoutException {
AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false); AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false);
ProgressiveMediaPeriod.Listener sourceInfoRefreshListener = ProgressiveMediaPeriod.Listener sourceInfoRefreshListener =
(durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true); (durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true);
@ -88,7 +112,8 @@ public final class ProgressiveMediaPeriodTest {
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE), new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
/* customCacheKey= */ null, /* customCacheKey= */ null,
ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES, ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES,
imageDurationUs); imageDurationUs,
executor);
AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false); AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false);
AtomicBoolean sourceInfoRefreshCalledBeforeOnPrepared = new AtomicBoolean(false); AtomicBoolean sourceInfoRefreshCalledBeforeOnPrepared = new AtomicBoolean(false);
@ -111,4 +136,19 @@ public final class ProgressiveMediaPeriodTest {
assertThat(sourceInfoRefreshCalledBeforeOnPrepared.get()).isTrue(); assertThat(sourceInfoRefreshCalledBeforeOnPrepared.get()).isTrue();
} }
private class ExecutionTrackingThread extends Thread {
private final AtomicBoolean hasRun;
public ExecutionTrackingThread(Runnable runnable, AtomicBoolean hasRun) {
super(runnable, "TestExecutionTrackingThread");
this.hasRun = hasRun;
}
@Override
public void run() {
hasRun.set(true);
super.run();
}
}
} }