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:
parent
18033c9c1b
commit
d8498f3ecb
@ -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 =
|
||||
|
@ -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}.
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user