mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Clip live periods that get a duration and end position
When an ad is inserted into a live period with an unset duration, the live period needs to be wrapped with a `ClippingMediaPeriod` and then actually be clipped to the end position when the duration gets known. Without this the renderers will never see an EOS which prevents the reading/playing period from advancing. In the case of a server side inserted ad on the other hand, the actual clipping needs to be prevented to keep the current behavior for SSAI streams. In an SSAI stream, an ad inserted before the current position should not produce a snap back to the newly inserted ad. This is currently prevented in both places, when the updated timeline is handled to not disable the renderers, and when the `mediaPeriodQueue` updates the queued periods. This behaviour is preserved to not create side effects of this change. PiperOrigin-RevId: 742642715
This commit is contained in:
parent
f2d644b7b4
commit
c95544156d
@ -590,10 +590,10 @@ import java.util.List;
|
||||
newPeriodInfo.copyWithRequestedContentPositionUs(
|
||||
oldPeriodInfo.requestedContentPositionUs);
|
||||
|
||||
if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
|
||||
// The period duration changed. Remove all subsequent periods and check whether we read
|
||||
// beyond the new duration.
|
||||
if (oldPeriodInfo.durationUs != newPeriodInfo.durationUs) {
|
||||
// The period duration changed.
|
||||
periodHolder.updateClipping();
|
||||
// Check whether we've read beyond the new duration.
|
||||
long newDurationInRendererTime =
|
||||
newPeriodInfo.durationUs == C.TIME_UNSET
|
||||
? Long.MAX_VALUE
|
||||
@ -607,12 +607,19 @@ import java.util.List;
|
||||
periodHolder == prewarming
|
||||
&& (maxRendererPrewarmingPositionUs == C.TIME_END_OF_SOURCE
|
||||
|| maxRendererPrewarmingPositionUs >= newDurationInRendererTime);
|
||||
// Remove all subsequent periods.
|
||||
@MediaPeriodQueue.UpdatePeriodQueueResult int removeAfterResult = removeAfter(periodHolder);
|
||||
if (removeAfterResult != 0) {
|
||||
return removeAfterResult;
|
||||
}
|
||||
boolean isLivePeriodClippedForAd =
|
||||
oldPeriodInfo.durationUs == C.TIME_UNSET
|
||||
&& oldPeriodInfo.endPositionUs == C.TIME_END_OF_SOURCE
|
||||
&& newPeriodInfo.endPositionUs != C.TIME_UNSET
|
||||
&& newPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE;
|
||||
int result = 0;
|
||||
if (isReadingAndReadBeyondNewDuration) {
|
||||
if (isReadingAndReadBeyondNewDuration
|
||||
&& (oldPeriodInfo.durationUs != C.TIME_UNSET || isLivePeriodClippedForAd)) {
|
||||
result |= UPDATE_PERIOD_QUEUE_ALTERED_READING_PERIOD;
|
||||
}
|
||||
if (isPrewarmingAndReadBeyondNewDuration) {
|
||||
@ -667,7 +674,7 @@ import java.util.List;
|
||||
isFollowedByTransitionToSameStream,
|
||||
isLastInPeriod,
|
||||
isLastInWindow,
|
||||
isLastInTimeline);
|
||||
/* isFinal= */ isLastInTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1225,8 +1232,6 @@ import java.util.List;
|
||||
boolean isPrecededByTransitionFromSameStream) {
|
||||
timeline.getPeriodByUid(periodUid, period);
|
||||
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
|
||||
boolean isNextAdGroupPostrollPlaceholder =
|
||||
nextAdGroupIndex != C.INDEX_UNSET && period.isLivePostrollPlaceholder(nextAdGroupIndex);
|
||||
boolean clipPeriodAtContentDuration = false;
|
||||
if (nextAdGroupIndex == C.INDEX_UNSET) {
|
||||
// Clip SSAI streams when at the end of the period.
|
||||
@ -1248,9 +1253,13 @@ import java.util.List;
|
||||
boolean isFollowedByTransitionToSameStream =
|
||||
nextAdGroupIndex != C.INDEX_UNSET
|
||||
&& period.isServerSideInsertedAdGroup(nextAdGroupIndex)
|
||||
&& !isNextAdGroupPostrollPlaceholder;
|
||||
&& !period.isLivePostrollPlaceholder(nextAdGroupIndex);
|
||||
boolean isFollowedByServerSidePostRollPlaceholder =
|
||||
nextAdGroupIndex != C.INDEX_UNSET
|
||||
&& period.isLivePostrollPlaceholder(nextAdGroupIndex)
|
||||
&& period.isServerSideInsertedAdGroup(nextAdGroupIndex);
|
||||
long endPositionUs =
|
||||
nextAdGroupIndex != C.INDEX_UNSET && !isNextAdGroupPostrollPlaceholder
|
||||
nextAdGroupIndex != C.INDEX_UNSET && !isFollowedByServerSidePostRollPlaceholder
|
||||
? period.getAdGroupTimeUs(nextAdGroupIndex)
|
||||
: clipPeriodAtContentDuration ? period.durationUs : C.TIME_UNSET;
|
||||
long durationUs =
|
||||
|
@ -10595,7 +10595,6 @@ public final class ExoPlayerTest {
|
||||
player.release();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Checking old volume commands
|
||||
@Test
|
||||
public void isCommandAvailable_isTrueForAvailableCommands() {
|
||||
ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build();
|
||||
@ -14139,35 +14138,24 @@ public final class ExoPlayerTest {
|
||||
.build();
|
||||
// Live stream timeline with unassigned next ad group.
|
||||
AdPlaybackState initialAdPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ new Object(), /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdDurationsUs(new long[][] {new long[] {10 * C.MICROS_PER_SECOND}});
|
||||
new AdPlaybackState(/* adsId= */ new Object())
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true);
|
||||
// Updated timeline with ad group at 18 seconds.
|
||||
long firstSampleTimeUs = TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
Timeline initialTimeline =
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* periodCount= */ 1,
|
||||
/* id= */ 0,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
initialAdPlaybackState));
|
||||
TimelineWindowDefinition initialTimelineWindowDefinition =
|
||||
new TimelineWindowDefinition.Builder()
|
||||
.setDynamic(true)
|
||||
.setDurationUs(C.TIME_UNSET)
|
||||
.setUid(0)
|
||||
.setAdPlaybackStates(ImmutableList.of(initialAdPlaybackState))
|
||||
.build();
|
||||
Timeline initialTimeline = new FakeTimeline(initialTimelineWindowDefinition);
|
||||
AdPlaybackState updatedAdPlaybackState =
|
||||
initialAdPlaybackState.withAdGroupTimeUs(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adGroupTimeUs= */ firstSampleTimeUs + 18 * C.MICROS_PER_SECOND);
|
||||
Timeline updatedTimeline =
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* periodCount= */ 1,
|
||||
/* id= */ 0,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
updatedAdPlaybackState));
|
||||
initialAdPlaybackState
|
||||
.withNewAdGroup(0, firstSampleTimeUs + 18 * C.MICROS_PER_SECOND)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, new long[] {10 * C.MICROS_PER_SECOND});
|
||||
// Add samples to allow player to load and start playing (but no EOS as this is a live stream).
|
||||
FakeMediaSource mediaSource =
|
||||
new FakeMediaSource(
|
||||
@ -14181,16 +14169,26 @@ public final class ExoPlayerTest {
|
||||
|
||||
// Set updated ad group once we reach 20 seconds, and then continue playing until 40 seconds.
|
||||
player
|
||||
.createMessage((message, payload) -> mediaSource.setNewSourceInfo(updatedTimeline))
|
||||
.setPosition(20_000)
|
||||
.createMessage(
|
||||
(message, payload) ->
|
||||
mediaSource.setNewSourceInfo(
|
||||
new FakeTimeline(
|
||||
initialTimelineWindowDefinition
|
||||
.buildUpon()
|
||||
.setAdPlaybackStates(ImmutableList.of(updatedAdPlaybackState))
|
||||
.build())))
|
||||
.setPosition(20_000L)
|
||||
.send();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 40_000);
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 40_000L);
|
||||
Timeline timeline = player.getCurrentTimeline();
|
||||
player.release();
|
||||
|
||||
// Assert that the renderer hasn't been reset despite the inserted ad group.
|
||||
assertThat(videoRenderer.get().positionResetCount).isEqualTo(1);
|
||||
assertThat(timeline.getPeriod(0, new Timeline.Period()).adPlaybackState.adGroupCount)
|
||||
.isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -27,7 +27,9 @@ import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.D
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.os.Looper;
|
||||
@ -49,7 +51,9 @@ import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
import androidx.media3.exoplayer.source.MediaPeriod;
|
||||
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||
import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller;
|
||||
import androidx.media3.exoplayer.source.SampleStream;
|
||||
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
|
||||
import androidx.media3.exoplayer.source.TrackGroupArray;
|
||||
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
|
||||
import androidx.media3.exoplayer.source.ads.SinglePeriodAdTimeline;
|
||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||
@ -488,6 +492,38 @@ public final class MediaPeriodQueueTest {
|
||||
/* nextAdGroupIndex= */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getNextMediaPeriodInfo_singlePeriodLiveTimelineWithPostRollPlaceholder_returnsCorrectMediaPeriodInfo() {
|
||||
SinglePeriodTimeline liveContentTimeline =
|
||||
new SinglePeriodTimeline(
|
||||
C.TIME_UNSET,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* useLiveConfiguration= */ true,
|
||||
/* manifest= */ null,
|
||||
AD_MEDIA_ITEM);
|
||||
adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ new Object())
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false);
|
||||
SinglePeriodAdTimeline adTimeline =
|
||||
new SinglePeriodAdTimeline(liveContentTimeline, adPlaybackState);
|
||||
setupTimelines(adTimeline);
|
||||
|
||||
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
||||
/* periodUid= */ firstPeriodUid,
|
||||
/* startPositionUs= */ 0,
|
||||
/* requestedContentPositionUs= */ C.TIME_UNSET,
|
||||
/* endPositionUs= */ C.TIME_END_OF_SOURCE,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
/* isPrecededByTransitionFromSameStream= */ false,
|
||||
/* isFollowedByTransitionToSameStream= */ false,
|
||||
/* isLastInPeriod= */ false,
|
||||
/* isLastInWindow= */ false,
|
||||
/* isFinal= */ false,
|
||||
/* nextAdGroupIndex= */ 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void getNextMediaPeriodInfo_multiPeriodTimelineWithNoAdsAndNoPostrollPlaceholder() {
|
||||
@ -1194,6 +1230,92 @@ public final class MediaPeriodQueueTest {
|
||||
assertThat(getQueueLength()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
updateQueuedPeriods_adInsertedIntoPeriodWithUnsetDuration_bufferedPositionEndOfSource()
|
||||
throws InterruptedException, ExoPlaybackException {
|
||||
// Initial setup enqueues the live period with only the placeholder ad in place.
|
||||
adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ new Object())
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false);
|
||||
SinglePeriodTimeline liveTimeline =
|
||||
new SinglePeriodTimeline(
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* useLiveConfiguration= */ true,
|
||||
/* manifest= */ null,
|
||||
AD_MEDIA_ITEM);
|
||||
setupTimelines(new SinglePeriodAdTimeline(liveTimeline, adPlaybackState));
|
||||
enqueueNext();
|
||||
// The period needs to be prepared to get the actual buffered position from it.
|
||||
mediaPeriodQueue
|
||||
.getLoadingPeriod()
|
||||
.mediaPeriod
|
||||
.prepare(
|
||||
new MediaPeriod.Callback() {
|
||||
@Override
|
||||
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||
TrackGroupArray trackGroups = mediaPeriod.getTrackGroups();
|
||||
ExoTrackSelection[] selection = new ExoTrackSelection[trackGroups.length];
|
||||
SampleStream[] streams = new SampleStream[trackGroups.length];
|
||||
for (int i = 0; i < streams.length; i++) {
|
||||
streams[i] = mock(SampleStream.class);
|
||||
}
|
||||
try {
|
||||
when(trackSelector.selectTracks(any(), any(), any(), any()))
|
||||
.thenReturn(
|
||||
new TrackSelectorResult(
|
||||
new RendererConfiguration[0],
|
||||
selection,
|
||||
Tracks.EMPTY,
|
||||
/* info= */ null));
|
||||
} catch (ExoPlaybackException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
mediaPeriod.selectTracks(
|
||||
selection,
|
||||
new boolean[trackGroups.length],
|
||||
streams,
|
||||
new boolean[trackGroups.length],
|
||||
/* positionUs= */ 0L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||
},
|
||||
0L);
|
||||
// Ad inserted into timeline.
|
||||
adPlaybackState =
|
||||
adPlaybackState
|
||||
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 30_000_123L)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
|
||||
updateAdTimeline(/* mediaSourceIndex= */ 0);
|
||||
Timeline playlistTimeline = mediaSourceList.createTimeline();
|
||||
mediaPeriodQueue
|
||||
.getLoadingPeriod()
|
||||
.handlePrepared(/* playbackSpeed= */ 1.0f, playlistTimeline, /* playWhenReady= */ false);
|
||||
// Assume renderers have not yet read beyond the ad group timeUs.
|
||||
long maxRendererReadPositionUs = Renderer.DEFAULT_DURATION_TO_PROGRESS_US + 30_000_122L;
|
||||
|
||||
@MediaPeriodQueue.UpdatePeriodQueueResult
|
||||
int updateQueuedPeriodsResult =
|
||||
mediaPeriodQueue.updateQueuedPeriods(
|
||||
playlistTimeline,
|
||||
/* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
|
||||
/* maxRendererReadPositionUs= */ maxRendererReadPositionUs,
|
||||
/* maxRendererPrewarmingPositionUs= */ maxRendererReadPositionUs);
|
||||
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().mediaPeriod.getBufferedPositionUs())
|
||||
.isEqualTo(C.TIME_END_OF_SOURCE);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().info.durationUs).isEqualTo(30_000_123L);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().info.startPositionUs).isEqualTo(0);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().info.endPositionUs).isEqualTo(30_000_123L);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().getBufferedPositionUs()).isEqualTo(30_000_123L);
|
||||
assertThat(updateQueuedPeriodsResult).isEqualTo(0);
|
||||
assertThat(getQueueLength()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdPositionInSinglePeriodTimeline_resolvesToAd() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user