Add PlaybackLooperProvider and make it injectable for ExoPlayer

PiperOrigin-RevId: 683607682
This commit is contained in:
tianyifeng 2024-10-08 07:03:37 -07:00 committed by Copybara-Service
parent abfeea518e
commit fd48dd9ce8
4 changed files with 138 additions and 21 deletions

View File

@ -504,7 +504,7 @@ public interface ExoPlayer extends Player {
/* package */ long detachSurfaceTimeoutMs; /* package */ long detachSurfaceTimeoutMs;
/* package */ boolean pauseAtEndOfMediaItems; /* package */ boolean pauseAtEndOfMediaItems;
/* package */ boolean usePlatformDiagnostics; /* package */ boolean usePlatformDiagnostics;
@Nullable /* package */ Looper playbackLooper; @Nullable /* package */ PlaybackLooperProvider playbackLooperProvider;
/* package */ boolean buildCalled; /* package */ boolean buildCalled;
/* package */ boolean suppressPlaybackOnUnsuitableOutput; /* package */ boolean suppressPlaybackOnUnsuitableOutput;
/* package */ String playerName; /* package */ String playerName;
@ -1299,7 +1299,23 @@ public interface ExoPlayer extends Player {
@UnstableApi @UnstableApi
public Builder setPlaybackLooper(Looper playbackLooper) { public Builder setPlaybackLooper(Looper playbackLooper) {
checkState(!buildCalled); 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; return this;
} }

View File

@ -375,7 +375,7 @@ import java.util.concurrent.TimeoutException;
clock, clock,
playbackInfoUpdateListener, playbackInfoUpdateListener,
playerId, playerId,
builder.playbackLooper, builder.playbackLooperProvider,
preloadConfiguration); preloadConfiguration);
volume = 1; volume = 1;

View File

@ -27,10 +27,8 @@ import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.os.Process;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -192,7 +190,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final LoadControl loadControl; private final LoadControl loadControl;
private final BandwidthMeter bandwidthMeter; private final BandwidthMeter bandwidthMeter;
private final HandlerWrapper handler; private final HandlerWrapper handler;
@Nullable private final HandlerThread internalPlaybackThread; private final PlaybackLooperProvider playbackLooperProvider;
private final Looper playbackLooper; private final Looper playbackLooper;
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
@ -257,7 +255,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
Clock clock, Clock clock,
PlaybackInfoUpdateListener playbackInfoUpdateListener, PlaybackInfoUpdateListener playbackInfoUpdateListener,
PlayerId playerId, PlayerId playerId,
@Nullable Looper playbackLooper, @Nullable PlaybackLooperProvider playbackLooperProvider,
PreloadConfiguration preloadConfiguration) { PreloadConfiguration preloadConfiguration) {
this.playbackInfoUpdateListener = playbackInfoUpdateListener; this.playbackInfoUpdateListener = playbackInfoUpdateListener;
this.renderers = renderers; this.renderers = renderers;
@ -318,17 +316,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
new MediaSourceList( new MediaSourceList(
/* listener= */ this, analyticsCollector, applicationLooperHandler, playerId); /* listener= */ this, analyticsCollector, applicationLooperHandler, playerId);
if (playbackLooper != null) { this.playbackLooperProvider =
internalPlaybackThread = null; (playbackLooperProvider == null) ? new PlaybackLooperProvider() : playbackLooperProvider;
this.playbackLooper = playbackLooper; this.playbackLooper = this.playbackLooperProvider.obtainLooper();
} 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();
}
handler = clock.createHandler(this.playbackLooper, this); handler = clock.createHandler(this.playbackLooper, this);
} }
@ -1564,9 +1554,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadControl.onReleased(playerId); loadControl.onReleased(playerId);
setState(Player.STATE_IDLE); setState(Player.STATE_IDLE);
} finally { } finally {
if (internalPlaybackThread != null) { playbackLooperProvider.releaseLooper();
internalPlaybackThread.quit();
}
synchronized (this) { synchronized (this) {
released = true; released = true;
notifyAll(); notifyAll();

View File

@ -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.
*
* <p>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.
*
* <p>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;
}
}
}
}