diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 454d47a35c..65b81f2698 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -138,6 +138,7 @@ ([#8430](https://github.com/google/ExoPlayer/issues/8430)). * Remove `setVideoDecoderOutputBufferRenderer` from Player API. Use `setVideoSurfaceView` and `clearVideoSurfaceView` instead. + * Replace `PlayerMessage.setHandler` with `PlayerMessage.setLooper`. * Extractors: * Populate codecs string for H.264/AVC in MP4, Matroska and FLV streams to allow decoder capability checks based on codec profile/level diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 66c5c30d2a..047971bf85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -456,6 +456,9 @@ public interface ExoPlayer extends Player { /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); + /** Returns the {@link Clock} used for playback. */ + Clock getClock(); + /** @deprecated Use {@link #prepare()} instead. */ @Deprecated void retry(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 7c8f9addbb..4801879c62 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -70,7 +70,6 @@ import java.util.List; private final Handler playbackInfoUpdateHandler; private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener; private final ExoPlayerImplInternal internalPlayer; - private final Handler internalPlayerHandler; private final ListenerSet listeners; private final Timeline.Period period; private final List mediaSourceHolderSnapshots; @@ -79,6 +78,7 @@ import java.util.List; @Nullable private final AnalyticsCollector analyticsCollector; private final Looper applicationLooper; private final BandwidthMeter bandwidthMeter; + private final Clock clock; @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; @@ -149,6 +149,7 @@ import java.util.List; this.seekParameters = seekParameters; this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems; this.applicationLooper = applicationLooper; + this.clock = clock; repeatMode = Player.REPEAT_MODE_OFF; Player playerForListeners = wrappingPlayer != null ? wrappingPlayer : this; listeners = @@ -193,7 +194,6 @@ import java.util.List; applicationLooper, clock, playbackInfoUpdateListener); - internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } /** @@ -260,6 +260,11 @@ import java.util.List; return applicationLooper; } + @Override + public Clock getClock() { + return clock; + } + @Override public void addListener(Player.EventListener listener) { listeners.add(listener); @@ -755,7 +760,8 @@ import java.util.List; target, playbackInfo.timeline, getCurrentWindowIndex(), - internalPlayerHandler); + clock, + internalPlayer.getPlaybackLooper()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index dab8daa3c1..046149d135 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1468,7 +1468,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException { - if (message.getHandler().getLooper() == playbackLooper) { + if (message.getLooper() == playbackLooper) { deliverMessage(message); if (playbackInfo.playbackState == Player.STATE_READY || playbackInfo.playbackState == Player.STATE_BUFFERING) { @@ -1481,21 +1481,23 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void sendMessageToTargetThread(final PlayerMessage message) { - Handler handler = message.getHandler(); - if (!handler.getLooper().getThread().isAlive()) { + Looper looper = message.getLooper(); + if (!looper.getThread().isAlive()) { Log.w("TAG", "Trying to send message on a dead thread."); message.markAsProcessed(/* isDelivered= */ false); return; } - handler.post( - () -> { - try { - deliverMessage(message); - } catch (ExoPlaybackException e) { - Log.e(TAG, "Unexpected error delivering message on external thread.", e); - throw new RuntimeException(e); - } - }); + clock + .createHandler(looper, /* callback= */ null) + .post( + () -> { + try { + deliverMessage(message); + } catch (ExoPlaybackException e) { + Log.e(TAG, "Unexpected error delivering message on external thread.", e); + throw new RuntimeException(e); + } + }); } private void deliverMessage(PlayerMessage message) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 6f81a35dd8..36f562f7cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2; import android.os.Handler; +import android.os.Looper; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import java.util.concurrent.TimeoutException; @@ -55,11 +55,12 @@ public final class PlayerMessage { private final Target target; private final Sender sender; + private final Clock clock; private final Timeline timeline; private int type; @Nullable private Object payload; - private Handler handler; + private Looper looper; private int windowIndex; private long positionMs; private boolean deleteAfterDelivery; @@ -77,7 +78,8 @@ public final class PlayerMessage { * set to {@link Timeline#EMPTY}, any position can be specified. * @param defaultWindowIndex The default window index in the {@code timeline} when no other window * index is specified. - * @param defaultHandler The default handler to send the message on when no other handler is + * @param clock The {@link Clock}. + * @param defaultLooper The default {@link Looper} to send the message on when no other looper is * specified. */ public PlayerMessage( @@ -85,11 +87,13 @@ public final class PlayerMessage { Target target, Timeline timeline, int defaultWindowIndex, - Handler defaultHandler) { + Clock clock, + Looper defaultLooper) { this.sender = sender; this.target = target; this.timeline = timeline; - this.handler = defaultHandler; + this.looper = defaultLooper; + this.clock = clock; this.windowIndex = defaultWindowIndex; this.positionMs = C.TIME_UNSET; this.deleteAfterDelivery = true; @@ -142,22 +146,28 @@ public final class PlayerMessage { return payload; } + /** @deprecated Use {@link #setLooper(Looper)} instead. */ + @Deprecated + public PlayerMessage setHandler(Handler handler) { + return setLooper(handler.getLooper()); + } + /** - * Sets the handler the message is delivered on. + * Sets the {@link Looper} the message is delivered on. * - * @param handler A {@link Handler}. + * @param looper A {@link Looper}. * @return This message. * @throws IllegalStateException If {@link #send()} has already been called. */ - public PlayerMessage setHandler(Handler handler) { + public PlayerMessage setLooper(Looper looper) { Assertions.checkState(!isSent); - this.handler = handler; + this.looper = looper; return this; } - /** Returns the handler the message is delivered on. */ - public Handler getHandler() { - return handler; + /** Returns the {@link Looper} the message is delivered on. */ + public Looper getLooper() { + return looper; } /** @@ -287,19 +297,19 @@ public final class PlayerMessage { * Blocks until after the message has been delivered or the player is no longer able to deliver * the message. * - *

Note that this method can't be called if the current thread is the same thread used by the - * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + *

Note that this method must not be called if the current thread is the same thread used by + * the message {@link #getLooper() looper} as it would cause a deadlock. * * @return Whether the message was delivered successfully. * @throws IllegalStateException If this method is called before {@link #send()}. * @throws IllegalStateException If this method is called on the same thread used by the message - * handler set with {@link #setHandler(Handler)}. + * {@link #getLooper() looper}. * @throws InterruptedException If the current thread is interrupted while waiting for the message * to be delivered. */ public synchronized boolean blockUntilDelivered() throws InterruptedException { Assertions.checkState(isSent); - Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + Assertions.checkState(looper.getThread() != Thread.currentThread()); while (!isProcessed) { wait(); } @@ -310,14 +320,14 @@ public final class PlayerMessage { * Blocks until after the message has been delivered or the player is no longer able to deliver * the message or the specified timeout elapsed. * - *

Note that this method can't be called if the current thread is the same thread used by the - * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + *

Note that this method must not be called if the current thread is the same thread used by + * the message {@link #getLooper() looper} as it would cause a deadlock. * * @param timeoutMs The timeout in milliseconds. * @return Whether the message was delivered successfully. * @throws IllegalStateException If this method is called before {@link #send()}. * @throws IllegalStateException If this method is called on the same thread used by the message - * handler set with {@link #setHandler(Handler)}. + * {@link #getLooper() looper}. * @throws TimeoutException If the {@code timeoutMs} elapsed and this message has not been * delivered and the player is still able to deliver the message. * @throws InterruptedException If the current thread is interrupted while waiting for the message @@ -325,14 +335,8 @@ public final class PlayerMessage { */ public synchronized boolean blockUntilDelivered(long timeoutMs) throws InterruptedException, TimeoutException { - return blockUntilDelivered(timeoutMs, Clock.DEFAULT); - } - - @VisibleForTesting() - /* package */ synchronized boolean blockUntilDelivered(long timeoutMs, Clock clock) - throws InterruptedException, TimeoutException { Assertions.checkState(isSent); - Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + Assertions.checkState(looper.getThread() != Thread.currentThread()); long deadlineMs = clock.elapsedRealtime() + timeoutMs; long remainingMs = timeoutMs; @@ -340,11 +344,9 @@ public final class PlayerMessage { wait(remainingMs); remainingMs = deadlineMs - clock.elapsedRealtime(); } - if (!isProcessed) { throw new TimeoutException("Message delivery timed out."); } - return isDelivered; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 01716fc0fb..e89e05eb64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1202,6 +1202,11 @@ public class SimpleExoPlayer extends BasePlayer return player.getApplicationLooper(); } + @Override + public Clock getClock() { + return player.getClock(); + } + @Override public void addListener(Player.EventListener listener) { // Don't verify application thread. We allow calls to this method from any thread. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java index 70fd5445e1..41579f073c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java @@ -22,7 +22,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; -import android.os.Handler; import android.os.HandlerThread; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.util.Clock; @@ -55,9 +54,14 @@ public class PlayerMessageTest { PlayerMessage.Target target = (messageType, payload) -> {}; handlerThread = new HandlerThread("TestHandler"); handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); message = - new PlayerMessage(sender, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); + new PlayerMessage( + sender, + target, + Timeline.EMPTY, + /* defaultWindowIndex= */ 0, + clock, + handlerThread.getLooper()); } @After @@ -69,8 +73,7 @@ public class PlayerMessageTest { public void blockUntilDelivered_timesOut() throws Exception { when(clock.elapsedRealtime()).thenReturn(0L).thenReturn(TIMEOUT_MS * 2); - assertThrows( - TimeoutException.class, () -> message.send().blockUntilDelivered(TIMEOUT_MS, clock)); + assertThrows(TimeoutException.class, () -> message.send().blockUntilDelivered(TIMEOUT_MS)); // Ensure blockUntilDelivered() entered the blocking loop. verify(clock, Mockito.times(2)).elapsedRealtime(); @@ -82,7 +85,7 @@ public class PlayerMessageTest { message.send().markAsProcessed(/* isDelivered= */ true); - assertThat(message.blockUntilDelivered(TIMEOUT_MS, clock)).isTrue(); + assertThat(message.blockUntilDelivered(TIMEOUT_MS)).isTrue(); } @Test @@ -110,7 +113,7 @@ public class PlayerMessageTest { }); try { - assertThat(message.blockUntilDelivered(TIMEOUT_MS, clock)).isTrue(); + assertThat(message.blockUntilDelivered(TIMEOUT_MS)).isTrue(); // Ensure blockUntilDelivered() entered the blocking loop. verify(clock, Mockito.atLeast(2)).elapsedRealtime(); future.get(1, SECONDS); diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java index 55a55ab059..fe67af3d93 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.robolectric; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; -import android.os.Handler; import android.os.Looper; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -297,20 +296,22 @@ public class TestPlayerRunHelper { public static void playUntilPosition(ExoPlayer player, int windowIndex, long positionMs) throws TimeoutException { verifyMainTestThread(player); - Handler testHandler = Util.createHandlerForCurrentOrMainLooper(); - + Looper applicationLooper = Util.getCurrentOrMainLooper(); AtomicBoolean messageHandled = new AtomicBoolean(false); player .createMessage( (messageType, payload) -> { // Block playback thread until pause command has been sent from test thread. ConditionVariable blockPlaybackThreadCondition = new ConditionVariable(); - testHandler.post( - () -> { - player.pause(); - messageHandled.set(true); - blockPlaybackThreadCondition.open(); - }); + player + .getClock() + .createHandler(applicationLooper, /* callback= */ null) + .post( + () -> { + player.pause(); + messageHandled.set(true); + blockPlaybackThreadCondition.open(); + }); try { blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { @@ -354,7 +355,7 @@ public class TestPlayerRunHelper { AtomicBoolean receivedMessageCallback = new AtomicBoolean(false); player .createMessage((type, data) -> receivedMessageCallback.set(true)) - .setHandler(Util.createHandlerForCurrentOrMainLooper()) + .setLooper(Util.getCurrentOrMainLooper()) .send(); runMainLooperUntil(receivedMessageCallback::get); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ca514432f2..fb0ee74bae 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; +import android.os.Looper; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -603,7 +603,7 @@ public abstract class Action { } else { message.setPosition(positionMs); } - message.setHandler(Util.createHandlerForCurrentOrMainLooper()); + message.setLooper(Util.getCurrentOrMainLooper()); message.setDeleteAfterDelivery(deleteAfterDelivery); message.send(); } @@ -685,18 +685,21 @@ public abstract class Action { @Nullable Surface surface, HandlerWrapper handler, @Nullable ActionNode nextAction) { - Handler testThreadHandler = Util.createHandlerForCurrentOrMainLooper(); // Schedule a message on the playback thread to ensure the player is paused immediately. + Looper applicationLooper = Util.getCurrentOrMainLooper(); player .createMessage( (messageType, payload) -> { // Block playback thread until pause command has been sent from test thread. ConditionVariable blockPlaybackThreadCondition = new ConditionVariable(); - testThreadHandler.post( - () -> { - player.pause(); - blockPlaybackThreadCondition.open(); - }); + player + .getClock() + .createHandler(applicationLooper, /* callback= */ null) + .post( + () -> { + player.pause(); + blockPlaybackThreadCondition.open(); + }); try { blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { @@ -712,7 +715,7 @@ public abstract class Action { (messageType, payload) -> nextAction.schedule(player, trackSelector, surface, handler)) .setPosition(windowIndex, positionMs) - .setHandler(testThreadHandler) + .setLooper(applicationLooper) .send(); } player.play(); @@ -1049,7 +1052,7 @@ public abstract class Action { player .createMessage( (type, data) -> nextAction.schedule(player, trackSelector, surface, handler)) - .setHandler(Util.createHandlerForCurrentOrMainLooper()) + .setLooper(Util.getCurrentOrMainLooper()) .send(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 3748f697fe..1eb4450fb6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Clock; import java.util.List; /** @@ -75,6 +76,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public Clock getClock() { + throw new UnsupportedOperationException(); + } + @Override public void addListener(Player.EventListener listener) { throw new UnsupportedOperationException();