Skip to content

Commit

Permalink
Resolve media period ids in multi-period windows
Browse files Browse the repository at this point in the history
Ignorable ad periods are skipped to resolve the media period id with the
ad playback state of the resulting period. In case of a change in the period
position un-played ad periods are rolled forward to be played.

PiperOrigin-RevId: 428011116
  • Loading branch information
marcbaechinger authored and icbaker committed Feb 21, 2022
1 parent 449a840 commit 0367faf
Show file tree
Hide file tree
Showing 12 changed files with 1,177 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.TimelineAsserts;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;

Expand Down Expand Up @@ -221,7 +222,7 @@ public void roundTripViaBundle_ofTimeline_yieldsEqualInstanceExceptIdsAndManifes
/* durationUs= */ 2,
/* defaultPositionUs= */ 22,
/* windowOffsetInFirstPeriodUs= */ 222,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder().setMediaId("mediaId2").build()),
new TimelineWindowDefinition(
/* periodCount= */ 3,
Expand All @@ -233,7 +234,7 @@ public void roundTripViaBundle_ofTimeline_yieldsEqualInstanceExceptIdsAndManifes
/* durationUs= */ 3,
/* defaultPositionUs= */ 33,
/* windowOffsetInFirstPeriodUs= */ 333,
AdPlaybackState.NONE,
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder().setMediaId("mediaId3").build()));

Timeline restoredTimeline = Timeline.CREATOR.fromBundle(timeline.toBundle());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1178,7 +1178,7 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti
requestedContentPositionUs =
seekPosition.windowPositionUs == C.TIME_UNSET ? C.TIME_UNSET : resolvedContentPositionUs;
periodId =
queue.resolveMediaPeriodIdForAds(
queue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
playbackInfo.timeline, periodUid, resolvedContentPositionUs);
if (periodId.isAd()) {
playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
Expand Down Expand Up @@ -1492,7 +1492,7 @@ private Pair<MediaPeriodId, Long> getPlaceholderFirstMediaPeriodPositionUs(Timel
window, period, firstWindowIndex, /* windowPositionUs= */ C.TIME_UNSET);
// Add ad metadata if any and propagate the window sequence number to new period id.
MediaPeriodId firstPeriodId =
queue.resolveMediaPeriodIdForAds(
queue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, firstPeriodAndPositionUs.first, /* positionUs= */ 0);
long positionUs = firstPeriodAndPositionUs.second;
if (firstPeriodId.isAd()) {
Expand Down Expand Up @@ -2354,7 +2354,7 @@ private void updateIsLoading() {
private PlaybackInfo handlePositionDiscontinuity(
MediaPeriodId mediaPeriodId,
long positionUs,
long contentPositionUs,
long requestedContentPositionUs,
long discontinuityStartPositionUs,
boolean reportDiscontinuity,
@DiscontinuityReason int discontinuityReason) {
Expand All @@ -2379,9 +2379,9 @@ private PlaybackInfo handlePositionDiscontinuity(
staticMetadata = extractMetadataFromTrackSelectionArray(trackSelectorResult.selections);
// Ensure the media period queue requested content position matches the new playback info.
if (playingPeriodHolder != null
&& playingPeriodHolder.info.requestedContentPositionUs != contentPositionUs) {
&& playingPeriodHolder.info.requestedContentPositionUs != requestedContentPositionUs) {
playingPeriodHolder.info =
playingPeriodHolder.info.copyWithRequestedContentPositionUs(contentPositionUs);
playingPeriodHolder.info.copyWithRequestedContentPositionUs(requestedContentPositionUs);
}
} else if (!mediaPeriodId.equals(playbackInfo.periodId)) {
// Reset previously kept track info if unprepared and the period changes.
Expand All @@ -2395,7 +2395,7 @@ private PlaybackInfo handlePositionDiscontinuity(
return playbackInfo.copyWithNewPosition(
mediaPeriodId,
positionUs,
contentPositionUs,
requestedContentPositionUs,
discontinuityStartPositionUs,
getTotalBufferedDurationUs(),
trackGroupArray,
Expand Down Expand Up @@ -2668,7 +2668,8 @@ private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange(

// Ensure ad insertion metadata is up to date.
MediaPeriodId periodIdWithAds =
queue.resolveMediaPeriodIdForAds(timeline, newPeriodUid, contentPositionForAdResolutionUs);
queue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, newPeriodUid, contentPositionForAdResolutionUs);
boolean earliestCuePointIsUnchangedOrLater =
periodIdWithAds.nextAdGroupIndex == C.INDEX_UNSET
|| (oldPeriodId.nextAdGroupIndex != C.INDEX_UNSET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.max;

import android.os.Handler;
Expand Down Expand Up @@ -446,21 +447,7 @@ public MediaPeriodId resolveMediaPeriodIdForAds(
Timeline timeline, Object periodUid, long positionUs) {
long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid);
return resolveMediaPeriodIdForAds(
timeline, periodUid, positionUs, windowSequenceNumber, period);
}

// Internal methods.

private void notifyQueueUpdate() {
ImmutableList.Builder<MediaPeriodId> builder = ImmutableList.builder();
@Nullable MediaPeriodHolder period = playing;
while (period != null) {
builder.add(period.info.id);
period = period.getNext();
}
@Nullable MediaPeriodId readingPeriodId = reading == null ? null : reading.info.id;
analyticsCollectorHandler.post(
() -> analyticsCollector.updateMediaPeriodQueueInfo(builder.build(), readingPeriodId));
timeline, periodUid, positionUs, windowSequenceNumber, window, period);
}

/**
Expand All @@ -481,8 +468,21 @@ private static MediaPeriodId resolveMediaPeriodIdForAds(
Object periodUid,
long positionUs,
long windowSequenceNumber,
Timeline.Window window,
Timeline.Period period) {
timeline.getPeriodByUid(periodUid, period);
timeline.getWindow(period.windowIndex, window);
int periodIndex = timeline.getIndexOfPeriod(periodUid);
// Skip ignorable server side inserted ad periods.
while ((period.durationUs == 0
&& period.getAdGroupCount() > 0
&& period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount())
&& period.getAdGroupIndexForPositionUs(0) == C.INDEX_UNSET)
&& periodIndex++ < window.lastPeriodIndex) {
timeline.getPeriod(periodIndex, period, /* setIds= */ true);
periodUid = checkNotNull(period.uid);
}
timeline.getPeriodByUid(periodUid, period);
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
if (adGroupIndex == C.INDEX_UNSET) {
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
Expand All @@ -493,6 +493,55 @@ private static MediaPeriodId resolveMediaPeriodIdForAds(
}
}

/**
* Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
* played after a period position change, returning an identifier for an ad group if one needs to
* be played before the specified position, or an identifier for a content media period if not.
*
* @param timeline The timeline the period is part of.
* @param periodUid The uid of the timeline period to play.
* @param positionUs The next content position in the period to play.
* @return The identifier for the first media period to play, taking into account unplayed ads.
*/
public MediaPeriodId resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
Timeline timeline, Object periodUid, long positionUs) {
long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid);
// Check for preceding ad periods in multi-period window.
timeline.getPeriodByUid(periodUid, period);
timeline.getWindow(period.windowIndex, window);
Object periodUidToPlay = periodUid;
boolean seenAdPeriod = false;
for (int i = timeline.getIndexOfPeriod(periodUid); i >= window.firstPeriodIndex; i--) {
timeline.getPeriod(/* periodIndex= */ i, period, /* setIds= */ true);
boolean isAdPeriod = period.getAdGroupCount() > 0;
seenAdPeriod |= isAdPeriod;
if (period.getAdGroupIndexForPositionUs(period.durationUs) != C.INDEX_UNSET) {
// Roll forward to preceding un-played ad period.
periodUidToPlay = checkNotNull(period.uid);
}
if (seenAdPeriod && (!isAdPeriod || period.durationUs != 0)) {
// Stop for any periods except un-played ads with no content.
break;
}
}
return resolveMediaPeriodIdForAds(
timeline, periodUidToPlay, positionUs, windowSequenceNumber, window, period);
}

// Internal methods.

private void notifyQueueUpdate() {
ImmutableList.Builder<MediaPeriodId> builder = ImmutableList.builder();
@Nullable MediaPeriodHolder period = playing;
while (period != null) {
builder.add(period.info.id);
period = period.getNext();
}
@Nullable MediaPeriodId readingPeriodId = reading == null ? null : reading.info.id;
analyticsCollectorHandler.post(
() -> analyticsCollector.updateMediaPeriodQueueInfo(builder.build(), readingPeriodId));
}

/**
* Resolves the specified period uid to a corresponding window sequence number. Either by reusing
* the window sequence number of an existing matching media period or by creating a new window
Expand Down Expand Up @@ -647,12 +696,12 @@ private MediaPeriodInfo getFollowingMediaPeriodInfo(
// We can't create a next period yet.
return null;
}

long startPositionUs;
long contentPositionUs;
// We either start a new period in the same window or the first period in the next window.
long startPositionUs = 0;
long contentPositionUs = 0;
int nextWindowIndex =
timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
Object nextPeriodUid = period.uid;
Object nextPeriodUid = checkNotNull(period.uid);
long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber;
if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
// We're starting to buffer a new window. When playback transitions to this window we'll
Expand All @@ -672,20 +721,32 @@ private MediaPeriodInfo getFollowingMediaPeriodInfo(
}
nextPeriodUid = defaultPositionUs.first;
startPositionUs = defaultPositionUs.second;
MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
@Nullable MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) {
windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber;
} else {
windowSequenceNumber = nextWindowSequenceNumber++;
}
} else {
// We're starting to buffer a new period within the same window.
startPositionUs = 0;
contentPositionUs = 0;
}

@Nullable
MediaPeriodId periodId =
resolveMediaPeriodIdForAds(
timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, period);
timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, window, period);
if (contentPositionUs != C.TIME_UNSET
&& mediaPeriodInfo.requestedContentPositionUs != C.TIME_UNSET) {
boolean isPrecedingPeriodAnAd =
timeline.getPeriodByUid(mediaPeriodInfo.id.periodUid, period).getAdGroupCount() > 0
&& period.isServerSideInsertedAdGroup(period.getRemovedAdGroupCount());
// Handle the requested content position for period transitions within the same window.
if (periodId.isAd() && isPrecedingPeriodAnAd) {
// Propagate the requested position to the following ad period in the same window.
contentPositionUs = mediaPeriodInfo.requestedContentPositionUs;
} else if (isPrecedingPeriodAnAd) {
// Use the requested content position of the preceding ad period as the start position.
startPositionUs = mediaPeriodInfo.requestedContentPositionUs;
}
}
return getMediaPeriodInfo(timeline, periodId, contentPositionUs, startPositionUs);
}

Expand Down
Loading

0 comments on commit 0367faf

Please sign in to comment.