Use Clock to create Handler for delivering messages.

This ensures the message devilery is governed by the clock.

Also replace setting a Handler with a Looper to facilititate this
change.

PiperOrigin-RevId: 353019729
This commit is contained in:
tonihei 2021-01-21 15:58:29 +00:00 committed by kim-vde
parent a10e9de484
commit 4cbd4e2e2a
10 changed files with 102 additions and 70 deletions

View file

@ -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

View file

@ -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();

View file

@ -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<Player.EventListener, Player.Events> listeners;
private final Timeline.Period period;
private final List<MediaSourceHolderSnapshot> 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

View file

@ -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 {

View file

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

View file

@ -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.

View file

@ -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);

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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();