Ability to set timeout on release() and setSurface()

Add experimental APIs to set a timeout Player#release() and
PlayerMessage#blockUntilDeliver().

PiperOrigin-RevId: 281479146
This commit is contained in:
christosts 2019-11-20 10:20:31 +00:00 committed by Oliver Woodman
parent f6afbe6cb0
commit f921d0d3e5
5 changed files with 291 additions and 14 deletions

View File

@ -143,6 +143,8 @@ public interface ExoPlayer extends Player {
private boolean useLazyPreparation;
private boolean buildCalled;
private long releaseTimeoutMs;
/**
* Creates a builder with a list of {@link Renderer Renderers}.
*
@ -211,6 +213,21 @@ public interface ExoPlayer extends Player {
this.clock = clock;
}
/**
* Set a limit on the time a call to {@link ExoPlayer#release()} can spend. If a call to {@link
* ExoPlayer#release()} takes more than {@code timeoutMs} milliseconds to complete, the player
* will raise an error via {@link Player.EventListener#onPlayerError}.
*
* <p>This method is experimental, and will be renamed or removed in a future release. It should
* only be called before the player is used.
*
* @param timeoutMs The time limit in milliseconds, or 0 for no limit.
*/
public Builder experimental_setReleaseTimeoutMs(long timeoutMs) {
releaseTimeoutMs = timeoutMs;
return this;
}
/**
* Sets the {@link TrackSelector} that will be used by the player.
*
@ -317,8 +334,14 @@ public interface ExoPlayer extends Player {
public ExoPlayer build() {
Assertions.checkState(!buildCalled);
buildCalled = true;
return new ExoPlayerImpl(
renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
ExoPlayerImpl player =
new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
if (releaseTimeoutMs > 0) {
player.experimental_setReleaseTimeoutMs(releaseTimeoutMs);
}
return player;
}
}

View File

@ -36,6 +36,7 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeoutException;
/**
* An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayer.Builder}.
@ -144,6 +145,20 @@ import java.util.concurrent.CopyOnWriteArrayList;
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
}
/**
* Set a limit on the time a call to {@link #release()} can spend. If a call to {@link #release()}
* takes more than {@code timeoutMs} milliseconds to complete, the player will raise an error via
* {@link Player.EventListener#onPlayerError}.
*
* <p>This method is experimental, and will be renamed or removed in a future release. It should
* only be called before the player is used.
*
* @param timeoutMs The time limit in milliseconds, or 0 for no limit.
*/
public void experimental_setReleaseTimeoutMs(long timeoutMs) {
internalPlayer.experimental_setReleaseTimeoutMs(timeoutMs);
}
@Override
@Nullable
public AudioComponent getAudioComponent() {
@ -441,7 +456,13 @@ import java.util.concurrent.CopyOnWriteArrayList;
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
+ ExoPlayerLibraryInfo.registeredModules() + "]");
mediaSource = null;
internalPlayer.release();
if (!internalPlayer.release()) {
notifyListeners(
listener ->
listener.onPlayerError(
ExoPlaybackException.createForUnexpected(
new RuntimeException(new TimeoutException("Player release timed out.")))));
}
eventHandler.removeCallbacksAndMessages(null);
playbackInfo =
getResetPlaybackInfo(

View File

@ -123,6 +123,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
private int nextPendingMessageIndex;
private boolean deliverPendingMessageAtStartPositionRequired;
private long releaseTimeoutMs;
public ExoPlayerImplInternal(
Renderer[] renderers,
TrackSelector trackSelector,
@ -174,6 +176,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
deliverPendingMessageAtStartPositionRequired = true;
}
public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) {
this.releaseTimeoutMs = releaseTimeoutMs;
}
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
handler
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
@ -246,23 +252,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
public synchronized void release() {
public synchronized boolean release() {
if (released || !internalPlaybackThread.isAlive()) {
return;
return true;
}
handler.sendEmptyMessage(MSG_RELEASE);
boolean wasInterrupted = false;
while (!released) {
try {
wait();
} catch (InterruptedException e) {
wasInterrupted = true;
try {
if (releaseTimeoutMs > 0) {
waitUntilReleased(releaseTimeoutMs);
} else {
waitUntilReleased();
}
}
if (wasInterrupted) {
// Restore the interrupted status.
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return released;
}
public Looper getPlaybackLooper() {
@ -411,6 +417,63 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Private methods.
/**
* Blocks the current thread until {@link #releaseInternal()} is executed on the playback Thread.
*
* <p>If the current thread is interrupted while waiting for {@link #releaseInternal()} to
* complete, this method will delay throwing the {@link InterruptedException} to ensure that the
* underlying resources have been released, and will an {@link InterruptedException} <b>after</b>
* {@link #releaseInternal()} is complete.
*
* @throws {@link InterruptedException} if the current Thread was interrupted while waiting for
* {{@link #releaseInternal()}} to complete.
*/
private synchronized void waitUntilReleased() throws InterruptedException {
InterruptedException interruptedException = null;
while (!released) {
try {
wait();
} catch (InterruptedException e) {
interruptedException = e;
}
}
if (interruptedException != null) {
throw interruptedException;
}
}
/**
* Blocks the current thread until {@link #releaseInternal()} is performed on the playback Thread
* or the specified amount of time has elapsed.
*
* <p>If the current thread is interrupted while waiting for {@link #releaseInternal()} to
* complete, this method will delay throwing the {@link InterruptedException} to ensure that the
* underlying resources have been released or the operation timed out, and will throw an {@link
* InterruptedException} afterwards.
*
* @param timeoutMs the time in milliseconds to wait for {@link #releaseInternal()} to complete.
* @throws {@link InterruptedException} if the current Thread was interrupted while waiting for
* {{@link #releaseInternal()}} to complete.
*/
private synchronized void waitUntilReleased(long timeoutMs) throws InterruptedException {
long deadlineMs = clock.elapsedRealtime() + timeoutMs;
long remainingMs = timeoutMs;
InterruptedException interruptedException = null;
while (!released && remainingMs > 0) {
try {
wait(remainingMs);
} catch (InterruptedException e) {
interruptedException = e;
}
remainingMs = deadlineMs - clock.elapsedRealtime();
}
if (interruptedException != null) {
throw interruptedException;
}
}
private void setState(int state) {
if (playbackInfo.playbackState != state) {
playbackInfo = playbackInfo.copyWithPlaybackState(state);

View File

@ -17,7 +17,10 @@ package com.google.android.exoplayer2;
import android.os.Handler;
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;
/**
* Defines a player message which can be sent with a {@link Sender} and received by a {@link
@ -285,6 +288,28 @@ public final class PlayerMessage {
return isDelivered;
}
/**
* Blocks until after the message has been delivered or the player is no longer able to deliver
* the message or the specified waiting time elapses.
*
* <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.
*
* @param timeoutMs the maximum time to wait 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)}.
* @throws TimeoutException If the waiting time 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
* to be delivered.
*/
public synchronized boolean experimental_blockUntilDelivered(long timeoutMs)
throws InterruptedException, TimeoutException {
return experimental_blockUntilDelivered(timeoutMs, Clock.DEFAULT);
}
/**
* Marks the message as processed. Should only be called by a {@link Sender} and may be called
* multiple times.
@ -298,4 +323,24 @@ public final class PlayerMessage {
isProcessed = true;
notifyAll();
}
@VisibleForTesting()
/* package */ synchronized boolean experimental_blockUntilDelivered(long timeoutMs, Clock clock)
throws InterruptedException, TimeoutException {
Assertions.checkState(isSent);
Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread());
long deadlineMs = clock.elapsedRealtime() + timeoutMs;
long remainingMs = timeoutMs;
while (!isProcessed && remainingMs > 0) {
wait(remainingMs);
remainingMs = deadlineMs - clock.elapsedRealtime();
}
if (!isProcessed) {
throw new TimeoutException("Message delivery timed out.");
}
return isDelivered;
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2019 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 com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
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;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
/** Unit test for {@link PlayerMessage}. */
@RunWith(AndroidJUnit4.class)
public class PlayerMessageTest {
private static final long TIMEOUT_MS = 10;
@Mock Clock clock;
private HandlerThread handlerThread;
private PlayerMessage message;
@Before
public void setUp() {
initMocks(this);
PlayerMessage.Sender sender = (message) -> {};
PlayerMessage.Target target = (messageType, payload) -> {};
handlerThread = new HandlerThread("TestHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
message =
new PlayerMessage(sender, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler);
}
@After
public void tearDown() {
handlerThread.quit();
}
@Test
public void experimental_blockUntilDelivered_timesOut() throws Exception {
when(clock.elapsedRealtime()).thenReturn(0L).thenReturn(TIMEOUT_MS * 2);
try {
message.send().experimental_blockUntilDelivered(TIMEOUT_MS, clock);
fail();
} catch (TimeoutException expected) {
}
// Ensure experimental_blockUntilDelivered() entered the blocking loop
verify(clock, Mockito.times(2)).elapsedRealtime();
}
@Test
public void experimental_blockUntilDelivered_onAlreadyProcessed_succeeds() throws Exception {
when(clock.elapsedRealtime()).thenReturn(0L);
message.send().markAsProcessed(/* isDelivered= */ true);
assertThat(message.experimental_blockUntilDelivered(TIMEOUT_MS, clock)).isTrue();
}
@Test
public void experimental_blockUntilDelivered_markAsProcessedWhileBlocked_succeeds()
throws Exception {
message.send();
// Use a separate Thread to mark the message as processed.
CountDownLatch prepareLatch = new CountDownLatch(1);
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Boolean> future =
executorService.submit(
() -> {
prepareLatch.await();
message.markAsProcessed(true);
return true;
});
when(clock.elapsedRealtime())
.thenReturn(0L)
.then(
(invocation) -> {
// Signal the background thread to call PlayerMessage#markAsProcessed.
prepareLatch.countDown();
return TIMEOUT_MS - 1;
});
try {
assertThat(message.experimental_blockUntilDelivered(TIMEOUT_MS, clock)).isTrue();
// Ensure experimental_blockUntilDelivered() entered the blocking loop.
verify(clock, Mockito.atLeast(2)).elapsedRealtime();
future.get(1, TimeUnit.SECONDS);
} finally {
executorService.shutdown();
}
}
}