Fix order of timeline and prepare callback in MaskingMediaSoure

Once we receive an update from a masked source, we first start the
preparation of an already pending period, and only then notify the
player of the new timeline. If the period prepares immediately inline,
the MediaPeriod.onPrepared callback arrives before the
onPlaylistUpdateRequested call in the Player. THis is the wrong order
and causes issues when the player tries to lookup information in the
timeline that doesn't exist yet.

This change fixes preroll playbacks before live streams.

PiperOrigin-RevId: 293340031
This commit is contained in:
tonihei 2020-02-05 12:12:28 +00:00 committed by kim-vde
parent a9507a0064
commit c9245c61de
3 changed files with 67 additions and 4 deletions

View File

@ -141,6 +141,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Override @Override
protected synchronized void onChildSourceInfoRefreshed( protected synchronized void onChildSourceInfoRefreshed(
Void id, MediaSource mediaSource, Timeline newTimeline) { Void id, MediaSource mediaSource, Timeline newTimeline) {
@Nullable MediaPeriodId idForMaskingPeriodPreparation = null;
if (isPrepared) { if (isPrepared) {
timeline = timeline.cloneWithUpdatedTimeline(newTimeline); timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
} else if (newTimeline.isEmpty()) { } else if (newTimeline.isEmpty()) {
@ -183,14 +184,17 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
if (unpreparedMaskingMediaPeriod != null) { if (unpreparedMaskingMediaPeriod != null) {
MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
maskingPeriod.overridePreparePositionUs(periodPositionUs); maskingPeriod.overridePreparePositionUs(periodPositionUs);
MediaPeriodId idInSource = idForMaskingPeriodPreparation =
maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid));
maskingPeriod.createPeriod(idInSource);
} }
} }
hasRealTimeline = true; hasRealTimeline = true;
isPrepared = true; isPrepared = true;
refreshSourceInfo(this.timeline); refreshSourceInfo(this.timeline);
if (idForMaskingPeriodPreparation != null) {
Assertions.checkNotNull(unpreparedMaskingMediaPeriod)
.createPeriod(idForMaskingPeriodPreparation);
}
} }
@Nullable @Nullable

View File

@ -2988,7 +2988,7 @@ public final class ExoPlayerTest {
/* isDynamic= */ false, /* isDynamic= */ false,
/* durationUs= */ 10_000_000, /* durationUs= */ 10_000_000,
adPlaybackState)); adPlaybackState));
final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline); FakeMediaSource fakeMediaSource = new FakeMediaSource(/* timeline= */ null);
AtomicReference<Player> playerReference = new AtomicReference<>(); AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET);
EventListener eventListener = EventListener eventListener =
@ -3011,6 +3011,59 @@ public final class ExoPlayerTest {
} }
}) })
.seek(/* positionMs= */ 5_000) .seek(/* positionMs= */ 5_000)
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(() -> fakeMediaSource.setNewSourceInfo(fakeTimeline))
.build();
new ExoPlayerTestRunner.Builder()
.setMediaSources(fakeMediaSource)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(contentStartPositionMs.get()).isAtLeast(5_000L);
}
@Test
public void contentWithoutInitialSeekStartsAtDefaultPositionAfterPrerollAd() throws Exception {
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs...= */ 0);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 5_000_000,
adPlaybackState));
FakeMediaSource fakeMediaSource = new FakeMediaSource(/* timeline= */ null);
AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET);
EventListener eventListener =
new EventListener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
contentStartPositionMs.set(playerReference.get().getContentPosition());
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder("contentWithoutInitialSeekStartsAtDefaultPositionAfterPrerollAd")
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
playerReference.set(player);
player.addListener(eventListener);
}
})
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(() -> fakeMediaSource.setNewSourceInfo(fakeTimeline))
.build(); .build();
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setMediaSources(fakeMediaSource) .setMediaSources(fakeMediaSource)

View File

@ -42,6 +42,7 @@ public final class FakeTimeline extends Timeline {
public final boolean isLive; public final boolean isLive;
public final boolean isPlaceholder; public final boolean isPlaceholder;
public final long durationUs; public final long durationUs;
public final long defaultPositionUs;
public final AdPlaybackState adPlaybackState; public final AdPlaybackState adPlaybackState;
/** /**
@ -59,6 +60,7 @@ public final class FakeTimeline extends Timeline {
/* isLive= */ false, /* isLive= */ false,
/* isPlaceholder= */ true, /* isPlaceholder= */ true,
/* durationUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ 0,
AdPlaybackState.NONE); AdPlaybackState.NONE);
} }
@ -126,6 +128,7 @@ public final class FakeTimeline extends Timeline {
/* isLive= */ isDynamic, /* isLive= */ isDynamic,
/* isPlaceholder= */ false, /* isPlaceholder= */ false,
durationUs, durationUs,
/* defaultPositionUs= */ 0,
adPlaybackState); adPlaybackState);
} }
@ -140,6 +143,7 @@ public final class FakeTimeline extends Timeline {
* @param isLive Whether the window is live. * @param isLive Whether the window is live.
* @param isPlaceholder Whether the window is a placeholder. * @param isPlaceholder Whether the window is a placeholder.
* @param durationUs The duration of the window in microseconds. * @param durationUs The duration of the window in microseconds.
* @param defaultPositionUs The default position of the window in microseconds.
* @param adPlaybackState The ad playback state. * @param adPlaybackState The ad playback state.
*/ */
public TimelineWindowDefinition( public TimelineWindowDefinition(
@ -150,6 +154,7 @@ public final class FakeTimeline extends Timeline {
boolean isLive, boolean isLive,
boolean isPlaceholder, boolean isPlaceholder,
long durationUs, long durationUs,
long defaultPositionUs,
AdPlaybackState adPlaybackState) { AdPlaybackState adPlaybackState) {
this.periodCount = periodCount; this.periodCount = periodCount;
this.id = id; this.id = id;
@ -158,6 +163,7 @@ public final class FakeTimeline extends Timeline {
this.isLive = isLive; this.isLive = isLive;
this.isPlaceholder = isPlaceholder; this.isPlaceholder = isPlaceholder;
this.durationUs = durationUs; this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.adPlaybackState = adPlaybackState; this.adPlaybackState = adPlaybackState;
} }
} }
@ -252,7 +258,7 @@ public final class FakeTimeline extends Timeline {
windowDefinition.isSeekable, windowDefinition.isSeekable,
windowDefinition.isDynamic, windowDefinition.isDynamic,
windowDefinition.isLive, windowDefinition.isLive,
/* defaultPositionUs= */ 0, windowDefinition.defaultPositionUs,
windowDefinition.durationUs, windowDefinition.durationUs,
periodOffsets[windowIndex], periodOffsets[windowIndex],
periodOffsets[windowIndex + 1] - 1, periodOffsets[windowIndex + 1] - 1,