Skip to content

Commit

Permalink
Make current period a placeholder when a live stream is reset
Browse files Browse the repository at this point in the history
In case the player is reset while a live stream is playing, the current
period needs to be a placeholder. This makes sure that the default start
position is used when the first live timeline arrives after re-preparing.

#minor-release

PiperOrigin-RevId: 539044360
  • Loading branch information
marcbaechinger authored and tof-tof committed Jun 9, 2023
1 parent a66f08b commit 71153a4
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1492,9 +1492,17 @@ private void resetInternal(
queue.clear();
shouldContinueLoading = false;

Timeline timeline = playbackInfo.timeline;
if (releaseMediaSourceList && timeline instanceof PlaylistTimeline) {
// Wrap the current live timeline to make sure the current period is marked as a placeholder
// to force resolving the default start position with the next timeline refresh.
timeline =
((PlaylistTimeline) playbackInfo.timeline)
.copyWithPlaceholderTimeline(mediaSourceList.getShuffleOrder());
}
playbackInfo =
new PlaybackInfo(
playbackInfo.timeline,
timeline,
mediaPeriodId,
requestedContentPositionUs,
/* discontinuityStartPositionUs= */ startPositionUs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ public Timeline createTimeline() {
return new PlaylistTimeline(mediaSourceHolders, shuffleOrder);
}

/** Returns the shuffle order */
public ShuffleOrder getShuffleOrder() {
return shuffleOrder;
}

// Internal methods.

private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import androidx.media3.common.C;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.ForwardingTimeline;
import androidx.media3.exoplayer.source.ShuffleOrder;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -39,23 +40,26 @@
public PlaylistTimeline(
Collection<? extends MediaSourceInfoHolder> mediaSourceInfoHolders,
ShuffleOrder shuffleOrder) {
this(getTimelines(mediaSourceInfoHolders), getUids(mediaSourceInfoHolders), shuffleOrder);
}

private PlaylistTimeline(Timeline[] timelines, Object[] uids, ShuffleOrder shuffleOrder) {
super(/* isAtomic= */ false, shuffleOrder);
int childCount = mediaSourceInfoHolders.size();
int childCount = timelines.length;
this.timelines = timelines;
firstPeriodInChildIndices = new int[childCount];
firstWindowInChildIndices = new int[childCount];
timelines = new Timeline[childCount];
uids = new Object[childCount];
this.uids = uids;
childIndexByUid = new HashMap<>();
int index = 0;
int windowCount = 0;
int periodCount = 0;
for (MediaSourceInfoHolder mediaSourceInfoHolder : mediaSourceInfoHolders) {
timelines[index] = mediaSourceInfoHolder.getTimeline();
for (Timeline timeline : timelines) {
this.timelines[index] = timeline;
firstWindowInChildIndices[index] = windowCount;
firstPeriodInChildIndices[index] = periodCount;
windowCount += timelines[index].getWindowCount();
periodCount += timelines[index].getPeriodCount();
uids[index] = mediaSourceInfoHolder.getUid();
windowCount += this.timelines[index].getWindowCount();
periodCount += this.timelines[index].getPeriodCount();
childIndexByUid.put(uids[index], index++);
}
this.windowCount = windowCount;
Expand Down Expand Up @@ -112,4 +116,40 @@ public int getWindowCount() {
public int getPeriodCount() {
return periodCount;
}

public PlaylistTimeline copyWithPlaceholderTimeline(ShuffleOrder shuffleOrder) {
Timeline[] newTimelines = new Timeline[timelines.length];
for (int i = 0; i < timelines.length; i++) {
newTimelines[i] =
new ForwardingTimeline(timelines[i]) {
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Period superPeriod = super.getPeriod(periodIndex, period, setIds);
superPeriod.isPlaceholder = true;
return superPeriod;
}
};
}
return new PlaylistTimeline(newTimelines, uids, shuffleOrder);
}

private static Object[] getUids(
Collection<? extends MediaSourceInfoHolder> mediaSourceInfoHolders) {
Object[] uids = new Object[mediaSourceInfoHolders.size()];
int i = 0;
for (MediaSourceInfoHolder holder : mediaSourceInfoHolders) {
uids[i++] = holder.getUid();
}
return uids;
}

private static Timeline[] getTimelines(
Collection<? extends MediaSourceInfoHolder> mediaSourceInfoHolders) {
Timeline[] timelines = new Timeline[mediaSourceInfoHolders.size()];
int i = 0;
for (MediaSourceInfoHolder holder : mediaSourceInfoHolders) {
timelines[i++] = holder.getTimeline();
}
return timelines;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.ClippingMediaSource;
import androidx.media3.exoplayer.source.ConcatenatingMediaSource;
import androidx.media3.exoplayer.source.ForwardingTimeline;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
Expand Down Expand Up @@ -190,6 +191,7 @@
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -1563,6 +1565,7 @@ public void run(ExoPlayer player) {
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);

Expand Down Expand Up @@ -1601,6 +1604,119 @@ public void stop_releasesMediaSource() throws Exception {
mediaSource.assertReleased();
}

@Test
public void stop_withLiveStream_currentPeriodIsPlaceholder() throws TimeoutException {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
FakeTimeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
AdPlaybackState.NONE));
player.addMediaSources(ImmutableList.of(new FakeMediaSource(fakeTimeline)));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
assertThat(
player
.getCurrentTimeline()
.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.isPlaceholder)
.isFalse();

player.stop();

TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
assertThat(
player
.getCurrentTimeline()
.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.isPlaceholder)
.isTrue();
player.release();
}

@Test
public void stop_withVodStream_currentPeriodIsPlaceholder() throws TimeoutException {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSources(ImmutableList.of(new FakeMediaSource()));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
assertThat(
player
.getCurrentTimeline()
.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.isPlaceholder)
.isFalse();

player.stop();

TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
assertThat(
player
.getCurrentTimeline()
.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.isPlaceholder)
.isTrue();
player.release();
}

@Test
public void playbackError_withLiveStream_currentPeriodIsPlaceholder() throws TimeoutException {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
FakeTimeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
AdPlaybackState.NONE));
FakeMediaSource fakeMediaSource =
new FakeMediaSource(fakeTimeline) {
@Override
public Timeline getInitialTimeline() {
return fakeTimeline;
}

@Override
public synchronized void prepareSourceInternal(
@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
throw new IllegalArgumentException();
}
};
player.addMediaSources(ImmutableList.of(fakeMediaSource));
player.prepare();
assertThat(
player
.getCurrentTimeline()
.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.isPlaceholder)
.isFalse();

runUntilError(player);

assertThat(
player
.getCurrentTimeline()
.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.isPlaceholder)
.isTrue();
player.release();
}

@Test
public void release_correctMasking() throws Exception {
int[] currentMediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
Expand Down Expand Up @@ -1935,9 +2051,11 @@ public void stopAndSeekAfterStopDoesNotResetTimeline() throws Exception {
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(placeholderTimeline, timeline);
testRunner.assertTimelinesSame(
placeholderTimeline, timeline, createPlaceholderWrapperTimeline(timeline));
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}

Expand All @@ -1952,7 +2070,7 @@ public void reprepareAfterPlaybackError() throws Exception {
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForPlaybackState(Player.STATE_IDLE)
.prepare()
.waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_READY)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
Expand All @@ -1965,10 +2083,13 @@ public void reprepareAfterPlaybackError() throws Exception {
} catch (ExoPlaybackException e) {
// Expected exception.
}
testRunner.assertTimelinesSame(placeholderTimeline, timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertTimelinesSame(
placeholderTimeline, timeline, createPlaceholderWrapperTimeline(timeline), timeline);
}

@Test
Expand Down Expand Up @@ -2002,11 +2123,28 @@ public void seekAndReprepareAfterPlaybackError_keepsSeekPositionAndTimeline() th
long positionWhenFullyReadyAfterReprepare = player.getCurrentPosition();
player.release();

// Ensure we don't receive further timeline updates when repreparing.
verify(mockListener)
verify(mockListener, times(4)).onTimelineChanged(any(), anyInt());
InOrder inOrder = inOrder(mockListener);
inOrder
.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());
inOrder.verify(mockListener).onPlaybackStateChanged(Player.STATE_BUFFERING);
// Source update at reset after playback exception (isPlaceholder=true)
inOrder
.verify(mockListener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder.verify(mockListener).onPlaybackStateChanged(Player.STATE_READY);
// Source update replacing wrapper timeline of reset (isPlaceholder=false)
inOrder
.verify(mockListener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder.verify(mockListener).onPlaybackStateChanged(Player.STATE_IDLE);
inOrder.verify(mockListener).onPlaybackStateChanged(Player.STATE_BUFFERING);
// Source update at second preparation
inOrder
.verify(mockListener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder.verify(mockListener).onPlaybackStateChanged(Player.STATE_READY);

assertThat(positionAfterSeekHandled).isEqualTo(50);
assertThat(positionAfterReprepareHandled).isEqualTo(50);
Expand Down Expand Up @@ -2264,10 +2402,16 @@ public void playbackErrorTwiceStillKeepsTimeline() throws Exception {
} catch (ExoPlaybackException e) {
// Expected exception.
}
testRunner.assertTimelinesSame(placeholderTimeline, timeline, placeholderTimeline, timeline);
testRunner.assertTimelinesSame(
placeholderTimeline,
timeline,
createPlaceholderWrapperTimeline(timeline),
placeholderTimeline,
timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
Expand Down Expand Up @@ -13273,4 +13417,19 @@ public PlaybackParameters getPlaybackParameters() {
private static ArgumentMatcher<Timeline> noUid(Timeline timeline) {
return argument -> timelinesAreSame(argument, timeline);
}

/**
* Creates a forwarding timeline that sets the {@link Timeline.Period#isPlaceholder} flag to true.
* This is what happens when the player is stopped or a playback exception is thrown.
*/
private static Timeline createPlaceholderWrapperTimeline(Timeline timeline) {
return new ForwardingTimeline(timeline) {
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Period superPeriod = super.getPeriod(periodIndex, period, setIds);
superPeriod.isPlaceholder = true;
return superPeriod;
}
};
}
}
Loading

0 comments on commit 71153a4

Please sign in to comment.