Commit c4b346e4 by christosts Committed by Oliver Woodman

Integrate playback speed control in ExoPlayerImplInternal

Issue: #4904
PiperOrigin-RevId: 337048010
parent f00584b0
......@@ -865,6 +865,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs();
playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
// Adjust live playback speed to new position.
if (playbackInfo.playWhenReady
&& isCurrentPeriodInMovingLiveWindow()
&& playbackInfo.playbackParameters.speed == 1f) {
float adjustedSpeed = livePlaybackSpeedControl.adjustPlaybackSpeed(getCurrentLiveOffsetUs());
if (mediaClock.getPlaybackParameters().speed != adjustedSpeed) {
mediaClock.setPlaybackParameters(playbackInfo.playbackParameters.withSpeed(adjustedSpeed));
}
}
}
private void doSomeWork() throws ExoPlaybackException, IOException {
......@@ -992,6 +1002,34 @@ import java.util.concurrent.atomic.AtomicBoolean;
TraceUtil.endSection();
}
private long getCurrentLiveOffsetUs() {
return getLiveOffsetUs(
playbackInfo.timeline, playbackInfo.periodId.periodUid, playbackInfo.positionUs);
}
private long getLiveOffsetUs(Timeline timeline, Object periodUid, long periodPositionUs) {
int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex;
timeline.getWindow(windowIndex, window);
if (window.windowStartTimeMs == C.TIME_UNSET || !window.isLive || !window.isDynamic) {
return C.TIME_UNSET;
}
return C.msToUs(window.getCurrentUnixTimeMs() - window.windowStartTimeMs)
- (periodPositionUs + period.getPositionInWindowUs());
}
private boolean isCurrentPeriodInMovingLiveWindow() {
return isInMovingLiveWindow(playbackInfo.timeline, playbackInfo.periodId);
}
private boolean isInMovingLiveWindow(Timeline timeline, MediaPeriodId mediaPeriodId) {
if (mediaPeriodId.isAd() || timeline.isEmpty()) {
return false;
}
int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
timeline.getWindow(windowIndex, window);
return window.isLive && window.isDynamic;
}
private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
handler.removeMessages(MSG_DO_SOME_WORK);
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
......@@ -1095,6 +1133,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED);
seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
periodPositionUs = newPeriodPositionUs;
updateLivePlaybackSpeedControl(
/* newTimeline= */ playbackInfo.timeline,
/* newPeriodId= */ periodId,
/* oldTimeline= */ playbackInfo.timeline,
/* oldPeriodId= */ playbackInfo.periodId,
/* positionForTargetOffsetOverrideUs= */ requestedContentPosition);
}
} finally {
playbackInfo =
......@@ -1646,14 +1690,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
return true;
}
// Renderers are ready and we're loading. Ask the LoadControl whether to transition.
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
int windowIndex =
playbackInfo.timeline.getPeriodByUid(queue.getPlayingPeriod().uid, period).windowIndex;
playbackInfo.timeline.getWindow(windowIndex, window);
long targetLiveOffsetUs =
window.isLive && window.isDynamic
isInMovingLiveWindow(playbackInfo.timeline, queue.getPlayingPeriod().info.id)
? livePlaybackSpeedControl.getTargetLiveOffsetUs()
: C.TIME_UNSET;
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
return bufferedToEnd
|| loadControl.shouldStartPlayback(
......@@ -1720,6 +1761,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
newPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState);
}
} finally {
updateLivePlaybackSpeedControl(
/* newTimeline= */ timeline,
newPeriodId,
/* oldTimeline= */ playbackInfo.timeline,
/* oldPeriodId= */ playbackInfo.periodId,
/* positionForTargetOffsetOverrideUs */ positionUpdate.setTargetLiveOffset
? newPositionUs
: C.TIME_UNSET);
if (periodPositionChanged
|| newRequestedContentPositionUs != playbackInfo.requestedContentPositionUs) {
playbackInfo =
......@@ -1737,6 +1786,36 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
private void updateLivePlaybackSpeedControl(
Timeline newTimeline,
MediaPeriodId newPeriodId,
Timeline oldTimeline,
MediaPeriodId oldPeriodId,
long positionForTargetOffsetOverrideUs) {
if (newTimeline.isEmpty() || !isInMovingLiveWindow(newTimeline, newPeriodId)) {
// Live playback speed control is unused.
return;
}
int windowIndex = newTimeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex;
newTimeline.getWindow(windowIndex, window);
livePlaybackSpeedControl.updateLiveConfiguration(window.mediaItem.liveConfiguration);
if (positionForTargetOffsetOverrideUs != C.TIME_UNSET) {
livePlaybackSpeedControl.overrideTargetLiveOffsetUs(
getLiveOffsetUs(newTimeline, newPeriodId.periodUid, positionForTargetOffsetOverrideUs));
} else {
Object windowUid = window.uid;
@Nullable Object oldWindowUid = null;
if (!oldTimeline.isEmpty()) {
int oldWindowIndex = oldTimeline.getPeriodByUid(oldPeriodId.periodUid, period).windowIndex;
oldWindowUid = oldTimeline.getWindow(oldWindowIndex, window).uid;
}
if (!Util.areEqual(oldWindowUid, windowUid)) {
// Reset overridden target live offset to media values if window changes.
livePlaybackSpeedControl.overrideTargetLiveOffsetUs(C.TIME_UNSET);
}
}
}
private long getMaxRendererReadPositionUs() {
MediaPeriodHolder readingHolder = queue.getReadingPeriod();
if (readingHolder == null) {
......@@ -1936,6 +2015,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
: Player.DISCONTINUITY_REASON_AD_INSERTION;
playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
updateLivePlaybackSpeedControl(
/* newTimeline= */ playbackInfo.timeline,
/* newPeriodId= */ newPlayingPeriodHolder.info.id,
/* oldTimeline= */ playbackInfo.timeline,
/* oldPeriodId= */ oldPlayingPeriodHolder.info.id,
/* positionForTargetOffsetOverrideUs= */ C.TIME_UNSET);
resetPendingPauseAtEndOfPeriod();
updatePlaybackPositions();
advancedPlayingPeriod = true;
......@@ -2281,7 +2366,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* periodPositionUs= */ 0,
/* requestedContentPositionUs= */ C.TIME_UNSET,
/* forceBufferingState= */ false,
/* endPlayback= */ true);
/* endPlayback= */ true,
/* setTargetLiveOffset= */ false);
}
MediaPeriodId oldPeriodId = playbackInfo.periodId;
Object newPeriodUid = oldPeriodId.periodUid;
......@@ -2295,6 +2381,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
int startAtDefaultPositionWindowIndex = C.INDEX_UNSET;
boolean forceBufferingState = false;
boolean endPlayback = false;
boolean setTargetLiveOffset = false;
if (pendingInitialSeekPosition != null) {
// Resolve initial seek position.
@Nullable
......@@ -2319,6 +2406,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
} else {
newPeriodUid = periodPosition.first;
newContentPositionUs = periodPosition.second;
// Use explicit initial seek as new target live offset.
setTargetLiveOffset = true;
}
forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED;
}
......@@ -2362,6 +2451,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
newPeriodUid = periodPosition.first;
newContentPositionUs = periodPosition.second;
// Use an explicitly requested content position as new target live offset.
setTargetLiveOffset = true;
}
}
......@@ -2410,7 +2501,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
return new PositionUpdateForPlaylistChange(
newPeriodId, periodPositionUs, newContentPositionUs, forceBufferingState, endPlayback);
newPeriodId,
periodPositionUs,
newContentPositionUs,
forceBufferingState,
endPlayback,
setTargetLiveOffset);
}
private static boolean shouldUseRequestedContentPosition(
......@@ -2673,18 +2769,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final long requestedContentPositionUs;
public final boolean forceBufferingState;
public final boolean endPlayback;
public final boolean setTargetLiveOffset;
public PositionUpdateForPlaylistChange(
MediaPeriodId periodId,
long periodPositionUs,
long requestedContentPositionUs,
boolean forceBufferingState,
boolean endPlayback) {
boolean endPlayback,
boolean setTargetLiveOffset) {
this.periodId = periodId;
this.periodPositionUs = periodPositionUs;
this.requestedContentPositionUs = requestedContentPositionUs;
this.forceBufferingState = forceBufferingState;
this.endPlayback = endPlayback;
this.setTargetLiveOffset = setTargetLiveOffset;
}
}
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
......@@ -70,6 +71,17 @@ public final class PlaybackParameters {
return timeMs * scaledUsPerMs;
}
/**
* Returns a copy with the given speed.
*
* @param speed The new speed.
* @return The copied playback parameters.
*/
@CheckResult
public PlaybackParameters withSpeed(float speed) {
return new PlaybackParameters(speed, pitch);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
......
......@@ -111,6 +111,7 @@ import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -8412,6 +8413,428 @@ public final class ExoPlayerTest {
.onStaticMetadataChanged(ImmutableList.of(videoFormat.metadata, audioFormat.metadata));
}
@Test
public void targetLiveOffsetInMedia_adjustsLiveOffsetToTargetOffset() throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
Player.EventListener mockListener = mock(Player.EventListener.class);
player.addListener(mockListener);
player.pause();
player.setMediaSource(new FakeMediaSource(timeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the media value.
assertThat(liveOffsetAtEnd).isIn(Range.closed(8_900L, 9_100L));
// Assert that none of these playback speed changes were reported.
verify(mockListener, never()).onPlaybackParametersChanged(any());
}
@Test
public void targetLiveOffsetInMedia_withInitialSeek_adjustsLiveOffsetToInitialSeek()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
player.pause();
player.seekTo(18_000);
player.setMediaSource(new FakeMediaSource(timeline), /* resetPosition= */ false);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Target should have been permanently adjusted to 2 seconds.
// (initial now = 20 seconds in live window, initial seek to 18 seconds)
assertThat(liveOffsetAtStart).isIn(Range.closed(1_900L, 2_100L));
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
}
@Test
public void targetLiveOffsetInMedia_withUserSeek_adjustsLiveOffsetToSeek() throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
player.pause();
player.setMediaSource(new FakeMediaSource(timeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Seek to a live offset of 2 seconds.
player.seekTo(18_000);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert the live offset adjustment was permanent.
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
}
@Test
public void targetLiveOffsetInMedia_withTimelineUpdate_adjustsLiveOffsetToLatestTimeline()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline initialTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
Timeline updatedTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs + 50_000),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(4_000).build()));
FakeMediaSource fakeMediaSource = new FakeMediaSource(initialTimeline);
player.pause();
player.setMediaSource(fakeMediaSource);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Play a bit and update configuration.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 55_000);
fakeMediaSource.setNewSourceInfo(updatedTimeline);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that adjustment uses target offset from the updated timeline.
assertThat(liveOffsetAtEnd).isIn(Range.closed(3_900L, 4_100L));
}
@Test
public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 20 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
Player.EventListener mockListener = mock(Player.EventListener.class);
player.addListener(mockListener);
player.pause();
player.setMediaSource(new FakeMediaSource(timeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 20 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(-100L, 100L));
player.setPlaybackParameters(new PlaybackParameters(/* speed */ 2.0f));
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that the player didn't adjust the live offset to the media value (9 seconds) and
// instead played the media with double speed (resulting in a negative live offset).
assertThat(liveOffsetAtEnd).isLessThan(0);
// Assert that user-set speed was reported
verify(mockListener).onPlaybackParametersChanged(new PlaybackParameters(2.0f));
}
@Test
public void
targetLiveOffsetInMedia_afterAutomaticPeriodTransition_adjustsLiveOffsetToTargetOffset()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 10_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline liveTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
player.pause();
player.addMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)));
player.addMediaSource(new FakeMediaSource(liveTimeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 1, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the media value.
assertThat(liveOffsetAtEnd).isIn(Range.closed(8_900L, 9_100L));
}
@Test
public void
targetLiveOffsetInMedia_afterSeekToDefaultPositionInOtherStream_adjustsLiveOffsetToMediaOffset()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline liveTimeline1 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
Timeline liveTimeline2 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(4_000).build()));
player.pause();
player.addMediaSource(new FakeMediaSource(liveTimeline1));
player.addMediaSource(new FakeMediaSource(liveTimeline2));
// Ensure we override the target live offset to a seek position in the first live stream.
player.seekTo(10_000);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Seek to default position in second stream.
player.next();
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 1, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the media value.
assertThat(liveOffsetAtEnd).isIn(Range.closed(3_900L, 4_100L));
}
@Test
public void
targetLiveOffsetInMedia_afterSeekToSpecificPositionInOtherStream_adjustsLiveOffsetToSeekPosition()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline liveTimeline1 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(9_000).build()));
Timeline liveTimeline2 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(4_000).build()));
player.pause();
player.addMediaSource(new FakeMediaSource(liveTimeline1));
player.addMediaSource(new FakeMediaSource(liveTimeline2));
// Ensure we override the target live offset to a seek position in the first live stream.
player.seekTo(10_000);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Seek to specific position in second stream (at 2 seconds live offset).
player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 18_000);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 1, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the seek.
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
}
@Test
public void noTargetLiveOffsetInMedia_doesNotAdjustLiveOffset() throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(new AutoAdvancingFakeClock(/* initialTimeMs= */ nowUnixTimeMs))
.build();
Timeline liveTimelineWithoutTargetLiveOffset =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ C.msToUs(windowStartUnixTimeMs),
AdPlaybackState.NONE,
new MediaItem.Builder().setUri(Uri.EMPTY).build()));
player.pause();
player.setMediaSource(new FakeMediaSource(liveTimelineWithoutTargetLiveOffset));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that live offset is still the same (i.e. unadjusted).
assertThat(liveOffsetAtEnd).isIn(Range.closed(11_900L, 12_100L));
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
......@@ -314,8 +314,10 @@ public final class FakeTimeline extends Timeline {
windowDefinition.mediaItem,
manifests[windowIndex],
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ windowDefinition.isLive
? C.usToMs(windowDefinition.windowOffsetInFirstPeriodUs)
: C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ windowDefinition.isLive ? 0 : C.TIME_UNSET,
windowDefinition.isSeekable,
windowDefinition.isDynamic,
windowDefinition.isLive,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment