Set signal on renderer once it's allowed to render start of stream

When a renderer is pre-enabled (while another playback is still
ongoing), we pass mayRenderStartOfStream=false to Renderer.enable.
This ensures we don't show any first frames while the previous media
is still playing.

Currently, we never tell the renderer when we actually stop playing
the previous media so that it could render the start of the stream,
because we allow this as soon as the renderer is in STATE_STARTED and
we assume that we have to be in STATE_STARTED to make this stream
transition.

While this assumption is true, there are also cases where we can't
start the renderers because they are not ready yet and the video
renderer can't become ready because it didn't render its first frame.
This effectively blocks playback forever.

The most direct way of solving this, is to tell the renderer that
playback has transitioned and that it is now allowed to render the
start of the stream. This means it can never get blocked as described
above.

PiperOrigin-RevId: 547727347
This commit is contained in:
tonihei 2023-07-13 10:02:47 +01:00 committed by Rohit Singh
parent 18033c9c1b
commit d8498f3ecb
6 changed files with 153 additions and 1 deletions

View File

@ -2265,10 +2265,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
resetPendingPauseAtEndOfPeriod();
updatePlaybackPositions();
allowRenderersToRenderStartOfStreams();
advancedPlayingPeriod = true;
}
}
private void allowRenderersToRenderStartOfStreams() {
TrackSelectorResult playingTracks = queue.getPlayingPeriod().getTrackSelectorResult();
for (int i = 0; i < renderers.length; i++) {
if (playingTracks.isRendererEnabled(i)) {
renderers[i].enableMayRenderStartOfStream();
}
}
}
private void resetPendingPauseAtEndOfPeriod() {
@Nullable MediaPeriodHolder playingPeriod = queue.getPlayingPeriod();
pendingPauseAtEndOfPeriod =

View File

@ -462,6 +462,15 @@ public interface Renderer extends PlayerMessage.Target {
default void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed)
throws ExoPlaybackException {}
/**
* Enables this renderer to render the start of the stream even if the state is not {@link
* #STATE_STARTED} yet.
*
* <p>This is used to update the value of {@code mayRenderStartOfStream} passed to {@link
* #enable}.
*/
default void enableMayRenderStartOfStream() {}
/**
* Incrementally renders the {@link SampleStream}.
*

View File

@ -280,6 +280,11 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
renderedFirstFrameAfterEnable = false;
}
@Override
public void enableMayRenderStartOfStream() {
mayRenderFirstFrameAfterEnableIfNotStarted = true;
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false;

View File

@ -605,6 +605,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
renderedFirstFrameAfterEnable = false;
}
@Override
public void enableMayRenderStartOfStream() {
mayRenderFirstFrameAfterEnableIfNotStarted = true;
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
super.onPositionReset(positionUs, joining);

View File

@ -62,6 +62,7 @@ import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUnt
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilError;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlayWhenReady;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
@ -129,6 +130,7 @@ import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.SystemClock;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
@ -178,6 +180,7 @@ import androidx.media3.test.utils.FakeTrackSelection;
import androidx.media3.test.utils.FakeTrackSelector;
import androidx.media3.test.utils.FakeVideoRenderer;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -201,6 +204,7 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@ -227,9 +231,15 @@ public final class ExoPlayerTest {
*/
private static final int TIMEOUT_MS = 10_000;
private static final String SAMPLE_URI = "asset://android_asset/media/mp4/sample.mp4";
private Context context;
private Timeline placeholderTimeline;
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
@ -12542,7 +12552,7 @@ public final class ExoPlayerTest {
MediaItem mediaItem =
new MediaItem.Builder()
.setMediaId("id")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.setUri(Uri.parse(SAMPLE_URI))
.setMediaMetadata(mediaMetadata)
.build();
player.setMediaItem(mediaItem);
@ -13729,6 +13739,110 @@ public final class ExoPlayerTest {
player.release();
}
@Test
public void pauseAtEndOfMediaItem_withSecondStreamDelayed_playsSuccessfully() throws Exception {
// Set allowed video joining time to zero so that the renderer is not automatically considered
// ready when we re-enable it at the transition.
ExoPlayer player =
new ExoPlayer.Builder(context)
.setRenderersFactory(
new DefaultRenderersFactory(context).setAllowedVideoJoiningTimeMs(0))
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setPauseAtEndOfMediaItems(true);
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
player.setVideoSurface(surface);
player.addMediaItem(MediaItem.fromUri(SAMPLE_URI));
Timeline timeline = new FakeTimeline();
AtomicBoolean allowStreamRead = new AtomicBoolean();
MediaSource delayedStreamSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
long startPositionUs =
-timeline
.getPeriodByUid(id.periodUid, new Timeline.Period())
.getPositionInWindowUs();
// Add enough samples to the source so that the decoder can't decode everything at once.
return new FakeMediaPeriod(
trackGroupArray,
allocator,
(format, mediaPerioid) ->
ImmutableList.of(
oneByteSample(startPositionUs, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(startPositionUs + 10_000),
oneByteSample(startPositionUs + 20_000),
oneByteSample(startPositionUs + 30_000),
oneByteSample(startPositionUs + 40_000),
oneByteSample(startPositionUs + 50_000),
oneByteSample(startPositionUs + 60_000),
oneByteSample(startPositionUs + 70_000),
oneByteSample(startPositionUs + 80_000),
oneByteSample(startPositionUs + 90_000),
oneByteSample(startPositionUs + 100_000),
END_OF_STREAM_ITEM),
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false) {
@Override
protected FakeSampleStream createSampleStream(
Allocator allocator,
@Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
Format initialFormat,
List<FakeSampleStreamItem> fakeSampleStreamItems) {
return new FakeSampleStream(
allocator,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
initialFormat,
fakeSampleStreamItems) {
@Override
public int readData(
FormatHolder formatHolder,
DecoderInputBuffer buffer,
@ReadFlags int readFlags) {
return allowStreamRead.get()
? super.readData(formatHolder, buffer, readFlags)
: C.RESULT_NOTHING_READ;
}
};
}
};
}
};
player.addMediaSource(delayedStreamSource);
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.play();
player.prepare();
runUntilPlayWhenReady(player, /* expectedPlayWhenReady= */ false);
player.play();
runUntilPlaybackState(player, Player.STATE_BUFFERING);
allowStreamRead.set(true);
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
surface.release();
// Verify that playback is paused at the end and buffered at the start of each item.
verify(listener, times(2))
.onPlayWhenReadyChanged(
/* playWhenReady= */ eq(false),
eq(Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM));
verify(listener, times(2)).onPlaybackStateChanged(Player.STATE_BUFFERING);
}
// Internal methods.
private void addWatchAsSystemFeature() {

View File

@ -25,6 +25,7 @@ import android.media.MediaFormat;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@ -76,6 +77,10 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
@Override
protected void before() throws Throwable {
if (Util.SDK_INT <= 19) {
// Codec config not supported with Robolectric on API <= 19. Skip rule set up step.
return;
}
configureCodecs(supportedMimeTypes);
}
@ -83,6 +88,10 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
protected void after() {
supportedMimeTypes.clear();
MediaCodecUtil.clearDecoderInfoCache();
if (Util.SDK_INT <= 19) {
// Codec config not supported with Robolectric on API <= 19. Skip rule tear down step.
return;
}
ShadowMediaCodecList.reset();
ShadowMediaCodec.clearCodecs();
}