Skip to content

Commit

Permalink
Support changing ad break positions.
Browse files Browse the repository at this point in the history
The player already supports changing durations of periods and ads.
The only thing not yet supported is a change in ad break positions
which changes the duration of clipped content ending in an ad break.

Adding support for this requires updating the end position in
MediaPeriodInfo and changing the clip end position of the respective
ClippingMediaPeriod.

Issue: #5067
PiperOrigin-RevId: 373139724
  • Loading branch information
tonihei authored and kim-vde committed May 11, 2021
1 parent 0a93cc2 commit 08fb7bd
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 27 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
* Remove `CastPlayer` specific playlist manipulation methods. Use
`setMediaItems`, `addMediaItems`, `removeMediaItem` and `moveMediaItem`
instead.
* Ad playback:
* Support changing ad break positions in the player logic
([#5067](https://github.com/google/ExoPlayer/issues/5067).

### 2.14.0 (2021-05-13)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.ads;

import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.common.collect.ObjectArrays.concat;
import static java.lang.Math.max;

import android.net.Uri;
Expand Down Expand Up @@ -315,7 +316,7 @@ private static String keyForField(@AdGroup.FieldNumber int field) {
new AdPlaybackState(
/* adsId= */ null,
/* adGroupTimesUs= */ new long[0],
/* adGroups= */ null,
/* adGroups= */ new AdGroup[0],
/* adResumePositionUs= */ 0L,
/* contentDurationUs= */ C.TIME_UNSET);

Expand Down Expand Up @@ -353,29 +354,23 @@ public AdPlaybackState(Object adsId, long... adGroupTimesUs) {
this(
adsId,
adGroupTimesUs,
/* adGroups= */ null,
createEmptyAdGroups(adGroupTimesUs.length),
/* adResumePositionUs= */ 0,
/* contentDurationUs= */ C.TIME_UNSET);
}

private AdPlaybackState(
@Nullable Object adsId,
long[] adGroupTimesUs,
@Nullable AdGroup[] adGroups,
AdGroup[] adGroups,
long adResumePositionUs,
long contentDurationUs) {
checkArgument(adGroups == null || adGroups.length == adGroupTimesUs.length);
checkArgument(adGroups.length == adGroupTimesUs.length);
this.adsId = adsId;
this.adGroupTimesUs = adGroupTimesUs;
this.adResumePositionUs = adResumePositionUs;
this.contentDurationUs = contentDurationUs;
adGroupCount = adGroupTimesUs.length;
if (adGroups == null) {
adGroups = new AdGroup[adGroupCount];
for (int i = 0; i < adGroupCount; i++) {
adGroups[i] = new AdGroup();
}
}
this.adGroups = adGroups;
}

Expand Down Expand Up @@ -440,6 +435,30 @@ public boolean isAdInErrorState(int adGroupIndex, int adIndexInAdGroup) {
return adGroup.states[adIndexInAdGroup] == AdPlaybackState.AD_STATE_ERROR;
}

/**
* Returns an instance with the specified ad group times.
*
* <p>If the number of ad groups differs, ad groups are either removed or empty ad groups are
* added.
*
* @param adGroupTimesUs The new ad group times, in microseconds.
* @return The updated ad playback state.
*/
@CheckResult
public AdPlaybackState withAdGroupTimesUs(long[] adGroupTimesUs) {
AdGroup[] adGroups =
adGroupTimesUs.length < adGroupCount
? Util.nullSafeArrayCopy(this.adGroups, adGroupTimesUs.length)
: adGroupTimesUs.length == adGroupCount
? this.adGroups
: concat(
this.adGroups,
createEmptyAdGroups(adGroupTimesUs.length - adGroupCount),
AdGroup.class);
return new AdPlaybackState(
adsId, adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}

/**
* Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.
* The ad count must be greater than zero.
Expand Down Expand Up @@ -679,12 +698,15 @@ public Bundle toBundle() {

private static AdPlaybackState fromBundle(Bundle bundle) {
@Nullable long[] adGroupTimesUs = bundle.getLongArray(keyForField(FIELD_AD_GROUP_TIMES_US));
if (adGroupTimesUs == null) {
adGroupTimesUs = new long[0];
}
@Nullable
ArrayList<Bundle> adGroupBundleList =
bundle.getParcelableArrayList(keyForField(FIELD_AD_GROUPS));
@Nullable AdGroup[] adGroups;
if (adGroupBundleList == null) {
adGroups = null;
adGroups = createEmptyAdGroups(adGroupTimesUs.length);
} else {
adGroups = new AdGroup[adGroupBundleList.size()];
for (int i = 0; i < adGroupBundleList.size(); i++) {
Expand All @@ -696,14 +718,18 @@ private static AdPlaybackState fromBundle(Bundle bundle) {
long contentDurationUs =
bundle.getLong(keyForField(FIELD_CONTENT_DURATION_US), /* defaultValue= */ C.TIME_UNSET);
return new AdPlaybackState(
/* adsId= */ null,
adGroupTimesUs == null ? new long[0] : adGroupTimesUs,
adGroups,
adResumePositionUs,
contentDurationUs);
/* adsId= */ null, adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}

private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}

private static AdGroup[] createEmptyAdGroups(int count) {
AdGroup[] adGroups = new AdGroup[count];
for (int i = 0; i < count; i++) {
adGroups[i] = new AdGroup();
}
return adGroups;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,38 @@ public void setAdErrorBeforeAdCount() {
assertThat(state.isAdInErrorState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)).isFalse();
}

@Test
public void withAdGroupTimesUs_removingGroups_keepsRemainingGroups() {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, new long[] {0, C.msToUs(10_000)})
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);

state = state.withAdGroupTimesUs(new long[] {C.msToUs(3_000)});

assertThat(state.adGroupCount).isEqualTo(1);
assertThat(state.adGroups[0].count).isEqualTo(2);
assertThat(state.adGroups[0].uris[1]).isSameInstanceAs(TEST_URI);
}

@Test
public void withAdGroupTimesUs_addingGroups_keepsExistingGroups() {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, new long[] {0, C.msToUs(10_000)})
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI)
.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);

state = state.withAdGroupTimesUs(new long[] {0, C.msToUs(3_000), C.msToUs(20_000)});

assertThat(state.adGroupCount).isEqualTo(3);
assertThat(state.adGroups[0].count).isEqualTo(2);
assertThat(state.adGroups[0].uris[1]).isSameInstanceAs(TEST_URI);
assertThat(state.adGroups[1].states[0]).isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
assertThat(state.adGroups[2].count).isEqualTo(C.INDEX_UNSET);
}

@Test
public void getFirstAdIndexToPlayIsZero() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1799,6 +1799,7 @@ timeline, rendererPositionUs, getMaxRendererReadPositionUs())) {
// Update the new playing media period info if it already exists.
if (periodHolder.info.id.equals(newPeriodId)) {
periodHolder.info = queue.getUpdatedMediaPeriodInfo(timeline, periodHolder.info);
periodHolder.updateClipping();
}
periodHolder = periodHolder.getNext();
}
Expand Down Expand Up @@ -2127,14 +2128,27 @@ private boolean hasReadingPeriodFinishedReading() {
Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
if (renderer.getStream() != sampleStream
|| (sampleStream != null && !renderer.hasReadStreamToEnd())) {
|| (sampleStream != null
&& !renderer.hasReadStreamToEnd()
&& !hasFinishedReadingClippedContent(renderer, readingPeriodHolder))) {
// The current reading period is still being read by at least one renderer.
return false;
}
}
return true;
}

private boolean hasFinishedReadingClippedContent(Renderer renderer, MediaPeriodHolder reading) {
MediaPeriodHolder nextPeriod = reading.getNext();
// We can advance the reading period early once the clipped content has been read beyond its
// clipped end time because we know there won't be any further samples. This shortcut is helpful
// in case the clipped end time was reduced and renderers already read beyond the new end time.
// But wait until the next period is actually prepared to allow a seamless transition.
return reading.info.id.nextAdGroupIndex != C.INDEX_UNSET
&& nextPeriod.prepared
&& renderer.getReadingPositionUs() >= nextPeriod.getStartPositionRendererTime();
}

private void setAllRendererStreamsFinal(long streamEndPositionUs) {
for (Renderer renderer : renderers) {
if (renderer.getStream() != null) {
Expand Down Expand Up @@ -2587,12 +2601,12 @@ private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange(
// Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
// the only change is that MediaPeriodId.nextAdGroupIndex increased. This postpones a potential
// discontinuity until we reach the former next ad group position.
boolean oldAndNewPeriodIdAreSame =
boolean onlyNextAdGroupIndexIncreased =
oldPeriodId.periodUid.equals(newPeriodUid)
&& !oldPeriodId.isAd()
&& !periodIdWithAds.isAd()
&& earliestCuePointIsUnchangedOrLater;
MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds;
MediaPeriodId newPeriodId = onlyNextAdGroupIndexIncreased ? oldPeriodId : periodIdWithAds;

long periodPositionUs = contentPositionForAdResolutionUs;
if (newPeriodId.isAd()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ public long applyTrackSelection(
/** Releases the media period. No other method should be called after the release. */
public void release() {
disableTrackSelectionsInResult();
releaseMediaPeriod(info.endPositionUs, mediaSourceList, mediaPeriod);
releaseMediaPeriod(mediaSourceList, mediaPeriod);
}

/**
Expand Down Expand Up @@ -357,6 +357,15 @@ public TrackSelectorResult getTrackSelectorResult() {
return trackSelectorResult;
}

/** Updates the clipping to {@link MediaPeriodInfo#endPositionUs} if required. */
public void updateClipping() {
if (mediaPeriod instanceof ClippingMediaPeriod) {
long endPositionUs =
info.endPositionUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : info.endPositionUs;
((ClippingMediaPeriod) mediaPeriod).updateClipping(/* startUs= */ 0, endPositionUs);
}
}

private void enableTrackSelectionsInResult() {
if (!isLoadingMediaPeriod()) {
return;
Expand Down Expand Up @@ -422,7 +431,7 @@ private static MediaPeriod createMediaPeriod(
long startPositionUs,
long endPositionUs) {
MediaPeriod mediaPeriod = mediaSourceList.createPeriod(id, allocator, startPositionUs);
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
if (endPositionUs != C.TIME_UNSET) {
mediaPeriod =
new ClippingMediaPeriod(
mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs);
Expand All @@ -431,10 +440,9 @@ private static MediaPeriod createMediaPeriod(
}

/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
private static void releaseMediaPeriod(
long endPositionUs, MediaSourceList mediaSourceList, MediaPeriod mediaPeriod) {
private static void releaseMediaPeriod(MediaSourceList mediaSourceList, MediaPeriod mediaPeriod) {
try {
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
if (mediaPeriod instanceof ClippingMediaPeriod) {
mediaSourceList.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
} else {
mediaSourceList.releasePeriod(mediaPeriod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ public boolean updateQueuedPeriods(
if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
// The period duration changed. Remove all subsequent periods and check whether we read
// beyond the new duration.
periodHolder.updateClipping();
long newDurationInRendererTime =
newPeriodInfo.durationUs == C.TIME_UNSET
? Long.MAX_VALUE
Expand Down Expand Up @@ -384,17 +385,21 @@ public MediaPeriodInfo getUpdatedMediaPeriodInfo(Timeline timeline, MediaPeriodI
boolean isLastInWindow = isLastInWindow(timeline, id);
boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod);
timeline.getPeriodByUid(info.id.periodUid, period);
long endPositionUs =
id.isAd() || id.nextAdGroupIndex == C.INDEX_UNSET
? C.TIME_UNSET
: period.getAdGroupTimeUs(id.nextAdGroupIndex);
long durationUs =
id.isAd()
? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup)
: (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE
: (endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
? period.getDurationUs()
: info.endPositionUs);
: endPositionUs);
return new MediaPeriodInfo(
id,
info.startPositionUs,
info.requestedContentPositionUs,
info.endPositionUs,
endPositionUs,
durationUs,
isLastInPeriod,
isLastInWindow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,31 @@ public void getNextMediaPeriodInfo_inMultiPeriodWindow_returnsCorrectMediaPeriod
/* nextAdGroupIndex= */ C.INDEX_UNSET);
}

@Test
public void
updateQueuedPeriods_withDurationChangeInPlayingContent_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
setupAdTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
enqueueNext(); // Content before first ad.
enqueueNext(); // First ad.
enqueueNext(); // Content between ads.

// Change position of first ad (= change duration of playing content before first ad).
updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000);
setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 3000;
boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs);

assertThat(changeHandled).isTrue();
assertThat(getQueueLength()).isEqualTo(1);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.endPositionUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
assertThat(mediaPeriodQueue.getPlayingPeriod().info.durationUs)
.isEqualTo(FIRST_AD_START_TIME_US - 2000);
}

@Test
public void
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
Expand Down

0 comments on commit 08fb7bd

Please sign in to comment.