diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java index 9bc5c39c73..ef5421c443 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java @@ -54,6 +54,7 @@ import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import androidx.media3.exoplayer.upstream.Loader; import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction; import androidx.media3.exoplayer.upstream.Loader.Loadable; +import androidx.media3.exoplayer.util.ReleasableExecutor; import androidx.media3.extractor.DiscardingTrackOutput; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.ExtractorOutput; @@ -70,7 +71,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Executor; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -173,8 +173,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. * @param singleSampleDurationUs The duration of media with a single sample in microseconds. - * @param downloadExecutor An optional externally provided {@link Executor} for loading and - * extracting media. + * @param downloadExecutor An optional externally provided {@link ReleasableExecutor} for loading + * and extracting media. */ // maybeFinishPrepare is not posted to the handler until initialization completes. @SuppressWarnings({"nullness:argument", "nullness:methodref.receiver.bound"}) @@ -191,7 +191,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, long singleSampleDurationUs, - @Nullable Executor downloadExecutor) { + @Nullable ReleasableExecutor downloadExecutor) { this.uri = uri; this.dataSource = dataSource; this.drmSessionManager = drmSessionManager; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java index 0710960667..fbcf0e4a03 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Timeline; +import androidx.media3.common.util.Consumer; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; @@ -34,6 +35,7 @@ import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; +import androidx.media3.exoplayer.util.ReleasableExecutor; import androidx.media3.extractor.DefaultExtractorsFactory; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.ExtractorsFactory; @@ -66,7 +68,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private DrmSessionManagerProvider drmSessionManagerProvider; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; - @Nullable private Supplier downloadExecutor; + @Nullable private Supplier downloadExecutorSupplier; /** * Creates a new factory for {@link ProgressiveMediaSource}s. @@ -203,13 +205,17 @@ public final class ProgressiveMediaSource extends BaseMediaSource /** * Sets a supplier for an {@link Executor} that is used for loading the media. * - * @param downloadExecutor A {@link Supplier} that provides an externally managed - * {@link Executor} for downloading and extraction. + * @param downloadExecutor A {@link Supplier} that provides an externally managed {@link + * Executor} for downloading and extraction. + * @param downloadExecutorReleaser A callback triggered once a load task is finished and a + * supplied executor is no longer required. * @return This factory, for convenience. */ @CanIgnoreReturnValue - public Factory setDownloadExecutor(Supplier downloadExecutor) { - this.downloadExecutor = downloadExecutor; + public Factory setDownloadExecutor( + Supplier downloadExecutor, Consumer downloadExecutorReleaser) { + this.downloadExecutorSupplier = + () -> ReleasableExecutor.from(downloadExecutor.get(), downloadExecutorReleaser); return this; } @@ -230,7 +236,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource drmSessionManagerProvider.get(mediaItem), loadErrorHandlingPolicy, continueLoadingCheckIntervalBytes, - downloadExecutor); + downloadExecutorSupplier); } @Override @@ -250,7 +256,9 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; private final int continueLoadingCheckIntervalBytes; - @Nullable private final Supplier downloadExecutor; + + @Nullable private final Supplier downloadExecutorSupplier; + private boolean timelineIsPlaceholder; private long timelineDurationUs; private boolean timelineIsSeekable; @@ -266,7 +274,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, int continueLoadingCheckIntervalBytes, - @Nullable Supplier downloadExecutor) { + @Nullable Supplier downloadExecutorSupplier) { this.mediaItem = mediaItem; this.dataSourceFactory = dataSourceFactory; this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory; @@ -275,7 +283,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.timelineIsPlaceholder = true; this.timelineDurationUs = C.TIME_UNSET; - this.downloadExecutor = downloadExecutor; + this.downloadExecutorSupplier = downloadExecutorSupplier; } @Override @@ -332,7 +340,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource localConfiguration.customCacheKey, continueLoadingCheckIntervalBytes, Util.msToUs(localConfiguration.imageDurationMs), - downloadExecutor != null ? downloadExecutor.get() : null); + downloadExecutorSupplier != null ? downloadExecutorSupplier.get() : null); } @Override diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java index 80aed38b96..e2ee724462 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java @@ -45,11 +45,11 @@ import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import androidx.media3.exoplayer.upstream.Loader; import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction; +import androidx.media3.exoplayer.util.ReleasableExecutor; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.Executor; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -120,8 +120,8 @@ public class ChunkSampleStream * events. * @param canReportInitialDiscontinuity Whether the stream can report an initial discontinuity if * the first chunk can't start at the beginning and needs to preroll data. - * @param downloadExecutor An optional externally provided {@link Executor} for loading and - * extracting media. + * @param downloadExecutor An optional externally provided {@link ReleasableExecutor} for loading + * and extracting media. */ public ChunkSampleStream( @C.TrackType int primaryTrackType, @@ -136,7 +136,7 @@ public class ChunkSampleStream LoadErrorHandlingPolicy loadErrorHandlingPolicy, MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, boolean canReportInitialDiscontinuity, - @Nullable Executor downloadExecutor) { + @Nullable ReleasableExecutor downloadExecutor) { this.primaryTrackType = primaryTrackType; this.embeddedTrackTypes = embeddedTrackTypes == null ? new int[0] : embeddedTrackTypes; this.embeddedTrackFormats = embeddedTrackFormats == null ? new Format[0] : embeddedTrackFormats; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java index 96610b2ad0..308619abea 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/Loader.java @@ -31,12 +31,12 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.TraceUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.util.ReleasableExecutor; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -218,8 +218,7 @@ public final class Loader implements LoaderErrorThrower { } } - private final Executor downloadExecutor; - private final Runnable downloadExecutorReleaser; + private final ReleasableExecutor downloadExecutor; @Nullable private LoadTask currentTask; @Nullable private IOException fatalError; @@ -231,20 +230,21 @@ public final class Loader implements LoaderErrorThrower { * component using the loader. */ public Loader(String threadNameSuffix) { - ExecutorService executorService = - Util.newSingleThreadExecutor(THREAD_NAME_PREFIX + threadNameSuffix); - this.downloadExecutor = executorService; - this.downloadExecutorReleaser = executorService::shutdown; + this( + /* downloadExecutor= */ ReleasableExecutor.from( + Util.newSingleThreadExecutor(THREAD_NAME_PREFIX + threadNameSuffix), + ExecutorService::shutdown)); } /** * Constructs an instance. * - * @param downloadExecutor An {@link Executor} for supplying the loader's thread. + * @param downloadExecutor A {@link ReleasableExecutor} to run the load task. The {@link + * ReleasableExecutor} will be {@linkplain ReleasableExecutor#release() released} once the + * loader no longer requires it for new load tasks. */ - public Loader(Executor downloadExecutor) { + public Loader(ReleasableExecutor downloadExecutor) { this.downloadExecutor = downloadExecutor; - this.downloadExecutorReleaser = () -> {}; } /** @@ -328,7 +328,7 @@ public final class Loader implements LoaderErrorThrower { if (callback != null) { downloadExecutor.execute(new ReleaseTask(callback)); } - downloadExecutorReleaser.run(); + downloadExecutor.release(); } // LoaderErrorThrower implementation. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/ReleasableExecutor.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/ReleasableExecutor.java new file mode 100644 index 0000000000..2aa980d8fa --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/ReleasableExecutor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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.util; + +import androidx.media3.common.util.Consumer; +import androidx.media3.common.util.UnstableApi; +import java.util.concurrent.Executor; + +/** + * An {@link Executor} with a dedicated {@link #release} method to signal when it is not longer + * needed. + */ +@UnstableApi +public interface ReleasableExecutor extends Executor { + + /** + * Releases the {@link Executor}, indicating that the caller no longer requires it for executing + * new commands. + * + *

When calling this method, there may still be pending commands that are currently executed. + */ + void release(); + + /** + * Creates a {@link ReleasableExecutor} from an {@link Executor} and a release callback. + * + * @param executor The {@link Executor} + * @param releaseCallback The release callback, accepting the {@code executor} as an argument. + * @return The releasable executor. + * @param The type of {@link Executor}. + */ + static ReleasableExecutor from(T executor, Consumer releaseCallback) { + return new ReleasableExecutor() { + @Override + public void execute(Runnable command) { + executor.execute(command); + } + + @Override + public void release() { + releaseCallback.accept(executor); + } + }; + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java index 4800de048e..b66afe4a11 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.util.Consumer; import androidx.media3.datasource.AssetDataSource; import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.analytics.PlayerId; @@ -29,10 +30,12 @@ import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; +import androidx.media3.exoplayer.util.ReleasableExecutor; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.mp4.Mp4Extractor; import androidx.media3.extractor.png.PngExtractor; +import androidx.media3.extractor.text.SubtitleParser; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.concurrent.Executor; @@ -72,25 +75,32 @@ public final class ProgressiveMediaPeriodTest { @Test public void supplyingCustomDownloadExecutor_downloadsOnCustomThread() throws TimeoutException { - AtomicBoolean hasThreadRunBefore = new AtomicBoolean(false); + AtomicBoolean hasThreadRun = new AtomicBoolean(false); + AtomicBoolean hasReleaseCallbackRun = new AtomicBoolean(false); Executor executor = - Executors.newSingleThreadExecutor( - (r) -> new ExecutionTrackingThread(r, hasThreadRunBefore)); + Executors.newSingleThreadExecutor(r -> new ExecutionTrackingThread(r, hasThreadRun)); testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( - new BundledExtractorsAdapter(Mp4Extractor.FACTORY), C.TIME_UNSET, executor); + new BundledExtractorsAdapter(Mp4Extractor.newFactory(SubtitleParser.Factory.UNSUPPORTED)), + C.TIME_UNSET, + executor, + e -> hasReleaseCallbackRun.set(true)); - assertThat(hasThreadRunBefore.get()).isTrue(); + assertThat(hasThreadRun.get()).isTrue(); + assertThat(hasReleaseCallbackRun.get()).isTrue(); } private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( ProgressiveMediaExtractor extractor, long imageDurationUs) throws TimeoutException { testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( - extractor, imageDurationUs, /* executor= */ null); + extractor, imageDurationUs, /* executor= */ null, /* executorReleased= */ null); } private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback( - ProgressiveMediaExtractor extractor, long imageDurationUs, @Nullable Executor executor) + ProgressiveMediaExtractor extractor, + long imageDurationUs, + @Nullable Executor executor, + @Nullable Consumer executorReleased) throws TimeoutException { AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false); ProgressiveMediaPeriod.Listener sourceInfoRefreshListener = @@ -112,7 +122,7 @@ public final class ProgressiveMediaPeriodTest { /* customCacheKey= */ null, ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES, imageDurationUs, - executor); + executor != null ? ReleasableExecutor.from(executor, executorReleased) : null); AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false); AtomicBoolean sourceInfoRefreshCalledBeforeOnPrepared = new AtomicBoolean(false);