Use correct period-window offset for initial prepare position.
MaskingMediaSource needs to resolve the prepare position set for a MaskingPeriod while the source was still unprepared to the first actual prepare position. It currently assumes that the period-window offset and the default position is zero. This assumption is correct when a PlaceholderTimeline is used, but it may not be true if the real timeline is already known (e.g. when re-preparing a live stream after a playback error). Fix this by using the known timeline at the time of the preparation. Also: - Update a test that should have caught this to use lazy re-preparation. - Change the demo app code to use the recommended way to restart playback after a BehindLiveWindowException. Issue: #8675 PiperOrigin-RevId: 361604191
This commit is contained in:
parent
ea0f72e46c
commit
bc9fb8615e
@ -27,6 +27,9 @@
|
|||||||
`ExoPlayer`.
|
`ExoPlayer`.
|
||||||
* Reset playback speed when live playback speed control becomes unused
|
* Reset playback speed when live playback speed control becomes unused
|
||||||
([#8664](https://github.com/google/ExoPlayer/issues/8664)).
|
([#8664](https://github.com/google/ExoPlayer/issues/8664)).
|
||||||
|
* Fix playback position issue when re-preparing playback after a
|
||||||
|
BehindLiveWindowException
|
||||||
|
([#8675](https://github.com/google/ExoPlayer/issues/8675)).
|
||||||
* Remove deprecated symbols:
|
* Remove deprecated symbols:
|
||||||
* Remove `Player.DefaultEventListener`. Use `Player.EventListener`
|
* Remove `Player.DefaultEventListener`. Use `Player.EventListener`
|
||||||
instead.
|
instead.
|
||||||
|
@ -440,8 +440,8 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onPlayerError(@NonNull ExoPlaybackException e) {
|
public void onPlayerError(@NonNull ExoPlaybackException e) {
|
||||||
if (isBehindLiveWindow(e)) {
|
if (isBehindLiveWindow(e)) {
|
||||||
clearStartPosition();
|
player.seekToDefaultPosition();
|
||||||
initializePlayer();
|
player.prepare();
|
||||||
} else {
|
} else {
|
||||||
updateButtonVisibility();
|
updateButtonVisibility();
|
||||||
showControls();
|
showControls();
|
||||||
|
@ -178,13 +178,17 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
|||||||
// anyway.
|
// anyway.
|
||||||
newTimeline.getWindow(/* windowIndex= */ 0, window);
|
newTimeline.getWindow(/* windowIndex= */ 0, window);
|
||||||
long windowStartPositionUs = window.getDefaultPositionUs();
|
long windowStartPositionUs = window.getDefaultPositionUs();
|
||||||
|
Object windowUid = window.uid;
|
||||||
if (unpreparedMaskingMediaPeriod != null) {
|
if (unpreparedMaskingMediaPeriod != null) {
|
||||||
long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();
|
long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();
|
||||||
if (periodPreparePositionUs != 0) {
|
timeline.getPeriodByUid(unpreparedMaskingMediaPeriod.id.periodUid, period);
|
||||||
windowStartPositionUs = periodPreparePositionUs;
|
long windowPreparePositionUs = period.getPositionInWindowUs() + periodPreparePositionUs;
|
||||||
|
long oldWindowDefaultPositionUs =
|
||||||
|
timeline.getWindow(/* windowIndex= */ 0, window).getDefaultPositionUs();
|
||||||
|
if (windowPreparePositionUs != oldWindowDefaultPositionUs) {
|
||||||
|
windowStartPositionUs = windowPreparePositionUs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object windowUid = window.uid;
|
|
||||||
Pair<Object, Long> periodPosition =
|
Pair<Object, Long> periodPosition =
|
||||||
newTimeline.getPeriodPosition(
|
newTimeline.getPeriodPosition(
|
||||||
window, period, /* windowIndex= */ 0, windowStartPositionUs);
|
window, period, /* windowIndex= */ 0, windowStartPositionUs);
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
|
|||||||
|
|
||||||
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
|
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
|
||||||
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow;
|
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow;
|
||||||
|
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
|
||||||
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
|
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
|
||||||
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState;
|
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState;
|
||||||
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
|
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
|
||||||
@ -1653,55 +1654,44 @@ public final class ExoPlayerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void seekAndReprepareAfterPlaybackError() throws Exception {
|
public void seekAndReprepareAfterPlaybackError_keepsSeekPositionAndTimeline() throws Exception {
|
||||||
Timeline timeline = new FakeTimeline();
|
SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
final long[] positionHolder = new long[2];
|
Player.EventListener mockListener = mock(Player.EventListener.class);
|
||||||
ActionSchedule actionSchedule =
|
player.addListener(mockListener);
|
||||||
new ActionSchedule.Builder(TAG)
|
FakeMediaSource fakeMediaSource = new FakeMediaSource();
|
||||||
.pause()
|
player.setMediaSource(fakeMediaSource);
|
||||||
.waitForPlaybackState(Player.STATE_READY)
|
|
||||||
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
|
|
||||||
.waitForPlaybackState(Player.STATE_IDLE)
|
|
||||||
.seek(/* positionMs= */ 50)
|
|
||||||
.waitForPendingPlayerCommands()
|
|
||||||
.executeRunnable(
|
|
||||||
new PlayerRunnable() {
|
|
||||||
@Override
|
|
||||||
public void run(SimpleExoPlayer player) {
|
|
||||||
positionHolder[0] = player.getCurrentPosition();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.prepare()
|
|
||||||
.waitForPlaybackState(Player.STATE_READY)
|
|
||||||
.executeRunnable(
|
|
||||||
new PlayerRunnable() {
|
|
||||||
@Override
|
|
||||||
public void run(SimpleExoPlayer player) {
|
|
||||||
positionHolder[1] = player.getCurrentPosition();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.play()
|
|
||||||
.build();
|
|
||||||
ExoPlayerTestRunner testRunner =
|
|
||||||
new ExoPlayerTestRunner.Builder(context)
|
|
||||||
.setTimeline(timeline)
|
|
||||||
.setActionSchedule(actionSchedule)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThrows(
|
player.prepare();
|
||||||
ExoPlaybackException.class,
|
runUntilPlaybackState(player, Player.STATE_READY);
|
||||||
() ->
|
player
|
||||||
testRunner
|
.createMessage(
|
||||||
.start()
|
(type, payload) -> {
|
||||||
.blockUntilActionScheduleFinished(TIMEOUT_MS)
|
throw ExoPlaybackException.createForSource(new IOException());
|
||||||
.blockUntilEnded(TIMEOUT_MS));
|
})
|
||||||
testRunner.assertTimelinesSame(placeholderTimeline, timeline);
|
.send();
|
||||||
testRunner.assertTimelineChangeReasonsEqual(
|
runUntilPlaybackState(player, Player.STATE_IDLE);
|
||||||
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
player.seekTo(/* positionMs= */ 50);
|
||||||
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
|
long positionAfterSeekHandled = player.getCurrentPosition();
|
||||||
assertThat(positionHolder[0]).isEqualTo(50);
|
// Delay re-preparation to force player to use its masking mechanisms.
|
||||||
assertThat(positionHolder[1]).isEqualTo(50);
|
fakeMediaSource.setAllowPreparation(false);
|
||||||
|
player.prepare();
|
||||||
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
|
long positionAfterReprepareHandled = player.getCurrentPosition();
|
||||||
|
fakeMediaSource.setAllowPreparation(true);
|
||||||
|
runUntilPlaybackState(player, Player.STATE_READY);
|
||||||
|
long positionWhenFullyReadyAfterReprepare = player.getCurrentPosition();
|
||||||
|
player.release();
|
||||||
|
|
||||||
|
// Ensure we don't receive further timeline updates when repreparing.
|
||||||
|
verify(mockListener)
|
||||||
|
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
|
||||||
|
verify(mockListener).onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
|
||||||
|
verify(mockListener, times(2)).onTimelineChanged(any(), anyInt());
|
||||||
|
|
||||||
|
assertThat(positionAfterSeekHandled).isEqualTo(50);
|
||||||
|
assertThat(positionAfterReprepareHandled).isEqualTo(50);
|
||||||
|
assertThat(positionWhenFullyReadyAfterReprepare).isEqualTo(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.testutil;
|
package com.google.android.exoplayer2.testutil;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
@ -86,6 +87,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||||||
private final ArrayList<MediaPeriodId> createdMediaPeriods;
|
private final ArrayList<MediaPeriodId> createdMediaPeriods;
|
||||||
private final DrmSessionManager drmSessionManager;
|
private final DrmSessionManager drmSessionManager;
|
||||||
|
|
||||||
|
private boolean preparationAllowed;
|
||||||
private @MonotonicNonNull Timeline timeline;
|
private @MonotonicNonNull Timeline timeline;
|
||||||
private boolean preparedSource;
|
private boolean preparedSource;
|
||||||
private boolean releasedSource;
|
private boolean releasedSource;
|
||||||
@ -154,6 +156,22 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||||||
this.createdMediaPeriods = new ArrayList<>();
|
this.createdMediaPeriods = new ArrayList<>();
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.trackDataFactory = trackDataFactory;
|
this.trackDataFactory = trackDataFactory;
|
||||||
|
preparationAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the next call to {@link #prepareSource} is allowed to finish. If not allowed, a
|
||||||
|
* later call to this method with {@code allowPreparation} set to true will finish the
|
||||||
|
* preparation.
|
||||||
|
*
|
||||||
|
* @param allowPreparation Whether preparation is allowed to finish.
|
||||||
|
*/
|
||||||
|
public synchronized void setAllowPreparation(boolean allowPreparation) {
|
||||||
|
preparationAllowed = allowPreparation;
|
||||||
|
if (allowPreparation && sourceInfoRefreshHandler != null) {
|
||||||
|
sourceInfoRefreshHandler.post(
|
||||||
|
() -> finishSourcePreparation(/* sendManifestLoadEvents= */ true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -204,7 +222,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||||||
preparedSource = true;
|
preparedSource = true;
|
||||||
releasedSource = false;
|
releasedSource = false;
|
||||||
sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper();
|
sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper();
|
||||||
if (timeline != null) {
|
if (preparationAllowed && timeline != null) {
|
||||||
finishSourcePreparation(/* sendManifestLoadEvents= */ true);
|
finishSourcePreparation(/* sendManifestLoadEvents= */ true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,11 +291,14 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||||||
* Sets a new timeline. If the source is already prepared, this triggers a source info refresh
|
* Sets a new timeline. If the source is already prepared, this triggers a source info refresh
|
||||||
* message being sent to the listener.
|
* message being sent to the listener.
|
||||||
*
|
*
|
||||||
|
* <p>Must only be called if preparation is {@link #setAllowPreparation(boolean) allowed}.
|
||||||
|
*
|
||||||
* @param newTimeline The new {@link Timeline}.
|
* @param newTimeline The new {@link Timeline}.
|
||||||
* @param sendManifestLoadEvents Whether to treat this as a manifest refresh and send manifest
|
* @param sendManifestLoadEvents Whether to treat this as a manifest refresh and send manifest
|
||||||
* load events to listeners.
|
* load events to listeners.
|
||||||
*/
|
*/
|
||||||
public synchronized void setNewSourceInfo(Timeline newTimeline, boolean sendManifestLoadEvents) {
|
public synchronized void setNewSourceInfo(Timeline newTimeline, boolean sendManifestLoadEvents) {
|
||||||
|
checkState(preparationAllowed);
|
||||||
if (sourceInfoRefreshHandler != null) {
|
if (sourceInfoRefreshHandler != null) {
|
||||||
sourceInfoRefreshHandler.post(
|
sourceInfoRefreshHandler.post(
|
||||||
() -> {
|
() -> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user