diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index a055a695ff..cc1f196eab 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -504,7 +504,7 @@ public interface ExoPlayer extends Player { /* package */ long detachSurfaceTimeoutMs; /* package */ boolean pauseAtEndOfMediaItems; /* package */ boolean usePlatformDiagnostics; - @Nullable /* package */ Looper playbackLooper; + @Nullable /* package */ PlaybackLooperProvider playbackLooperProvider; /* package */ boolean buildCalled; /* package */ boolean suppressPlaybackOnUnsuitableOutput; /* package */ String playerName; @@ -1299,7 +1299,23 @@ public interface ExoPlayer extends Player { @UnstableApi public Builder setPlaybackLooper(Looper playbackLooper) { checkState(!buildCalled); - this.playbackLooper = playbackLooper; + this.playbackLooperProvider = new PlaybackLooperProvider(playbackLooper); + return this; + } + + /** + * Sets the {@link PlaybackLooperProvider} that will be used for playback. + * + * @param playbackLooperProvider A {@link PlaybackLooperProvider}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @CanIgnoreReturnValue + @UnstableApi + @RestrictTo(LIBRARY_GROUP) + public Builder setPlaybackLooperProvider(PlaybackLooperProvider playbackLooperProvider) { + checkState(!buildCalled); + this.playbackLooperProvider = playbackLooperProvider; return this; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 02967b7b59..e76fa35f29 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -375,7 +375,7 @@ import java.util.concurrent.TimeoutException; clock, playbackInfoUpdateListener, playerId, - builder.playbackLooper, + builder.playbackLooperProvider, preloadConfiguration); volume = 1; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 14854a9284..f8e0031d8e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -27,10 +27,8 @@ import static java.lang.Math.max; import static java.lang.Math.min; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.os.Process; import android.util.Pair; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; @@ -192,7 +190,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private final LoadControl loadControl; private final BandwidthMeter bandwidthMeter; private final HandlerWrapper handler; - @Nullable private final HandlerThread internalPlaybackThread; + private final PlaybackLooperProvider playbackLooperProvider; private final Looper playbackLooper; private final Timeline.Window window; private final Timeline.Period period; @@ -257,7 +255,7 @@ import java.util.concurrent.atomic.AtomicBoolean; Clock clock, PlaybackInfoUpdateListener playbackInfoUpdateListener, PlayerId playerId, - @Nullable Looper playbackLooper, + @Nullable PlaybackLooperProvider playbackLooperProvider, PreloadConfiguration preloadConfiguration) { this.playbackInfoUpdateListener = playbackInfoUpdateListener; this.renderers = renderers; @@ -318,17 +316,9 @@ import java.util.concurrent.atomic.AtomicBoolean; new MediaSourceList( /* listener= */ this, analyticsCollector, applicationLooperHandler, playerId); - if (playbackLooper != null) { - internalPlaybackThread = null; - this.playbackLooper = playbackLooper; - } else { - // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can - // not normally change to this priority" is incorrect. - internalPlaybackThread = - new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); - internalPlaybackThread.start(); - this.playbackLooper = internalPlaybackThread.getLooper(); - } + this.playbackLooperProvider = + (playbackLooperProvider == null) ? new PlaybackLooperProvider() : playbackLooperProvider; + this.playbackLooper = this.playbackLooperProvider.obtainLooper(); handler = clock.createHandler(this.playbackLooper, this); } @@ -1564,9 +1554,7 @@ import java.util.concurrent.atomic.AtomicBoolean; loadControl.onReleased(playerId); setState(Player.STATE_IDLE); } finally { - if (internalPlaybackThread != null) { - internalPlaybackThread.quit(); - } + playbackLooperProvider.releaseLooper(); synchronized (this) { released = true; notifyAll(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackLooperProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackLooperProvider.java new file mode 100644 index 0000000000..f0051231ae --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackLooperProvider.java @@ -0,0 +1,113 @@ +/* + * 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 + * + * https://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; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; +import static androidx.media3.common.util.Assertions.checkState; + +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; +import androidx.annotation.GuardedBy; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Provides a {@link Looper} for multiple {@link ExoPlayer} instances with reference counting in + * order to properly manage the lifecycle of the thread that the {@link Looper} is associated with. + */ +@RestrictTo(LIBRARY_GROUP) +@UnstableApi +public final class PlaybackLooperProvider { + + private final Object lock; + + @GuardedBy("lock") + private @Nullable Looper playbackLooper; + + @GuardedBy("lock") + private @Nullable HandlerThread internalPlaybackThread; + + @GuardedBy("lock") + private int referenceCount; + + /** + * Creates an instance. + * + *
The {@link PlaybackLooperProvider} instance will create a {@link HandlerThread} internally + * and manage its lifecycle. + */ + public PlaybackLooperProvider() { + this(/* looper= */ null); + } + + /** + * Creates an instance. + * + * @param looper The {@linkplain Looper playback looper}. If non-null, the caller is responsible + * for managing the lifecycle of the thread that the {@code looper} is associated with. + * Otherwise, the {@link PlaybackLooperProvider} instance will create a {@linkplain + * HandlerThread playback thread} internally and manage its lifecycle. + */ + public PlaybackLooperProvider(@Nullable Looper looper) { + lock = new Object(); + playbackLooper = looper; + internalPlaybackThread = null; + referenceCount = 0; + } + + /** + * Obtains the {@linkplain Looper playback looper} by increasing the reference count. + * + * @return The playback looper. It will either be the {@code looper} injected via the constructor, + * or the {@linkplain HandlerThread#getLooper() looper} associated with the internal {@link + * HandlerThread playback thread}. + */ + public Looper obtainLooper() { + synchronized (lock) { + if (playbackLooper == null) { + checkState(referenceCount == 0 && internalPlaybackThread == null); + // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can + // not normally change to this priority" is incorrect. + internalPlaybackThread = + new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); + internalPlaybackThread.start(); + playbackLooper = internalPlaybackThread.getLooper(); + } + referenceCount++; + return playbackLooper; + } + } + + /** + * Releases the {@linkplain Looper playback looper} by decreasing the reference count. + * + *
If the playback looper was not provided by the caller, the {@link PlaybackLooperProvider} + * instance will automatically stop the internal {@link HandlerThread playback thread}. + */ + public void releaseLooper() { + synchronized (lock) { + checkState(referenceCount > 0); + referenceCount--; + if (referenceCount == 0 && internalPlaybackThread != null) { + internalPlaybackThread.quit(); + internalPlaybackThread = null; + playbackLooper = null; + } + } + } +}