Skip to content

Multi period content support for AdsMediaSource #2501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions demos/main/src/main/assets/media.exolist.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "MPD VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
"uri": "https://dash.akamaized.net/dash264/TestCases/5a/nomor/1.mpd",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ protected void onChildSourceInfoRefreshed(
.handleSourceInfoRefresh(newTimeline);
maybeUpdateSourceInfo();
} else {
Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
// Assertions.checkArgument(newTimeline.getPeriodCount() == 1);
contentTimeline = newTimeline;
mainHandler.post(
() -> {
Expand Down Expand Up @@ -471,7 +471,11 @@ private void maybeUpdateSourceInfo() {
refreshSourceInfo(contentTimeline);
} else {
adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs());
refreshSourceInfo(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
if (contentTimeline.getPeriodCount() == 1) {
refreshSourceInfo(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
} else {
refreshSourceInfo(new MultiPeriodAdTimeline(contentTimeline, adPlaybackState));
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.source.ads;

import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.exoplayer.source.ForwardingTimeline;


/**
* A custom {@link Timeline} for sources that have {@link AdPlaybackState} split among multiple periods.
* <br/>
* For each period a modified {@link AdPlaybackState} is created for each period:
* <ul>
* <li> ad group time is offset relative to period start time </li>
* <li> ad groups after period end time are marked as skipped </li>
* <li> post-roll ad group is kept only for last period </li>
* <li> ad group count and indices are kept unchanged </li>
* </ul>
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public final class MultiPeriodAdTimeline extends ForwardingTimeline {

private final AdPlaybackState[] adPlaybackStates;

/**
* Creates a new timeline with a single period containing ads.
*
* @param contentTimeline The timeline of the content alongside which ads will be played.
* @param adPlaybackState The state of the media's ads.
*/
public MultiPeriodAdTimeline(Timeline contentTimeline, AdPlaybackState adPlaybackState) {
super(contentTimeline);
final int periodCount = contentTimeline.getPeriodCount();
// for period count == 1 SinglePeriodAdTimeline should be used
Assertions.checkState(periodCount > 1);
Assertions.checkState(contentTimeline.getWindowCount() == 1);
this.adPlaybackStates = new AdPlaybackState[periodCount];

final Timeline.Period period = new Timeline.Period();
long periodStartOffsetUs = 0;
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
timeline.getPeriod(periodIndex, period);
final long periodDurationUs = period.durationUs;
adPlaybackStates[periodIndex] = forPeriod(adPlaybackState, periodStartOffsetUs,
periodDurationUs, periodIndex == periodCount - 1);
periodStartOffsetUs += periodDurationUs;
}
}

@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds);

period.set(
period.id,
period.uid,
period.windowIndex,
period.durationUs,
period.getPositionInWindowUs(),
adPlaybackStates[periodIndex],
period.isPlaceholder);
return period;
}

/**
* @param adPlaybackState original state is immutable always new modified copy is created
* @param periodStartOffsetUs period start time offset from start of timeline (microseconds)
* @param periodDurationUs period duration (microseconds)
* @param isLastPeriod true if this is the last period
* @return adPlaybackState modified for period
*/
private AdPlaybackState forPeriod(
AdPlaybackState adPlaybackState,
long periodStartOffsetUs,
long periodDurationUs,
boolean isLastPeriod) {
final long periodEndUs = periodStartOffsetUs + periodDurationUs;
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
final long adGroupTimeUs = adPlaybackState.getAdGroup(adGroupIndex).timeUs;
if (adGroupTimeUs == C.TIME_END_OF_SOURCE) {
if (!isLastPeriod) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);
}
} else {
if (periodEndUs < adGroupTimeUs) {
// this cue point belongs to next periods
adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);
}
// start time relative to period start
adPlaybackState = adPlaybackState.withAdGroupTimeUs(adGroupIndex,
adGroupTimeUs - periodStartOffsetUs);
}
}
return adPlaybackState;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package androidx.media3.exoplayer.source.ads;

import static androidx.media3.common.C.INDEX_UNSET;
import static androidx.media3.common.C.MICROS_PER_SECOND;
import static androidx.media3.common.C.TIME_END_OF_SOURCE;
import static androidx.media3.test.utils.FakeTimeline.FAKE_MEDIA_ITEM;
import static org.junit.Assert.assertEquals;

import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.Timeline.Period;
import androidx.media3.test.utils.FakeTimeline;
import org.junit.Test;

public class MultiPeriodAdTimelineTest {
@Test
public void getPeriod() {
String windowId = "windowId";

FakeTimeline contentTimeline = new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
3, // periodCount
windowId,
true, // isSeekable
false, // isDynamic
false, // isLive
false, // isPlaceholder
60 * MICROS_PER_SECOND, // durationUs
0, // defaultPositionUs
0, // windowOffsetInFirstPeriodUs
AdPlaybackState.NONE, // adPlaybackState
FAKE_MEDIA_ITEM // mediaItem
)
);

MultiPeriodAdTimeline multiPeriodAdTimeline = new MultiPeriodAdTimeline(
contentTimeline, new AdPlaybackState(
"adsId",
0L, 10 * MICROS_PER_SECOND, // period 0: 0s - 20s
25 * MICROS_PER_SECOND, 35 * MICROS_PER_SECOND, // period 1: 20s - 40s
45 * MICROS_PER_SECOND, 55 * MICROS_PER_SECOND // period 2: 40s - 60s
));

Period period0 = new Period();
multiPeriodAdTimeline.getPeriod(0, period0);

// period durations are uniformly split windowDuration/periodCount
assertEquals(20 * MICROS_PER_SECOND, period0.durationUs);

// positions within the 0th period
assertEquals(0, period0.getAdGroupIndexForPositionUs(1 * MICROS_PER_SECOND));
assertEquals(1, period0.getAdGroupIndexAfterPositionUs(1 * MICROS_PER_SECOND));
assertEquals(1, period0.getAdGroupIndexForPositionUs(19 * MICROS_PER_SECOND));
// no more ads to be played in 0th period
assertEquals(INDEX_UNSET, period0.getAdGroupIndexAfterPositionUs(19 * MICROS_PER_SECOND));

Period period1 = new Period();
multiPeriodAdTimeline.getPeriod(1, period1);

// positions within the 1st period
assertEquals(1, period1.getAdGroupIndexForPositionUs(1 * MICROS_PER_SECOND)); // 21s
assertEquals(2, period1.getAdGroupIndexAfterPositionUs(1 * MICROS_PER_SECOND)); // 21s
assertEquals(2, period1.getAdGroupIndexForPositionUs(10 * MICROS_PER_SECOND)); // 30s
assertEquals(3, period1.getAdGroupIndexAfterPositionUs(10 * MICROS_PER_SECOND)); // 30s
assertEquals(3, period1.getAdGroupIndexForPositionUs(19 * MICROS_PER_SECOND)); // 39s
// no more ads to be played in 1st period
assertEquals(INDEX_UNSET, period1.getAdGroupIndexAfterPositionUs(19 * MICROS_PER_SECOND)); // 39s

Period period2 = new Period();
multiPeriodAdTimeline.getPeriod(2, period2);

// positions within the 2nd period
assertEquals(3, period2.getAdGroupIndexForPositionUs(1 * MICROS_PER_SECOND)); // 41s
assertEquals(4, period2.getAdGroupIndexAfterPositionUs(1 * MICROS_PER_SECOND)); // 41s
assertEquals(4, period2.getAdGroupIndexForPositionUs(10 * MICROS_PER_SECOND)); // 50s
assertEquals(5, period2.getAdGroupIndexAfterPositionUs(10 * MICROS_PER_SECOND)); // 50s
assertEquals(5, period2.getAdGroupIndexForPositionUs(19 * MICROS_PER_SECOND)); // 59s
// no more ads to be played in 2nd period
assertEquals(INDEX_UNSET, period2.getAdGroupIndexAfterPositionUs(19 * MICROS_PER_SECOND)); // 59s
}

@Test
public void getPeriod_postRoll() {
String windowId = "windowId";

FakeTimeline contentTimeline = new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
2, // periodCount
windowId,
true, // isSeekable
false, // isDynamic
false, // isLive
false, // isPlaceholder
60 * MICROS_PER_SECOND, // durationUs
0, // defaultPositionUs
0, // windowOffsetInFirstPeriodUs
AdPlaybackState.NONE, // adPlaybackState
FAKE_MEDIA_ITEM // mediaItem
)
);

MultiPeriodAdTimeline multiPeriodAdTimeline = new MultiPeriodAdTimeline(
contentTimeline, new AdPlaybackState(
"adsId",
TIME_END_OF_SOURCE // period 1: 30s - 60s
));

Period period0 = new Period();
multiPeriodAdTimeline.getPeriod(0, period0);

// period durations are uniformly split windowDuration/periodCount
assertEquals(30 * MICROS_PER_SECOND, period0.durationUs);

assertEquals(INDEX_UNSET, period0.getAdGroupIndexForPositionUs(15 * MICROS_PER_SECOND));
// post-roll should not be played in 0th period
assertEquals(INDEX_UNSET, period0.getAdGroupIndexAfterPositionUs(15 * MICROS_PER_SECOND));

Period period1 = new Period();
multiPeriodAdTimeline.getPeriod(1, period1);

assertEquals(INDEX_UNSET, period1.getAdGroupIndexForPositionUs(29 * MICROS_PER_SECOND)); // 59s
// post-roll in the end
assertEquals(0, period1.getAdGroupIndexAfterPositionUs(29 * MICROS_PER_SECOND)); // 59s
}
}