-
Notifications
You must be signed in to change notification settings - Fork 765
Multi period content support for AdsMediaSource #3126
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
base: main
Are you sure you want to change the base?
Changes from 12 commits
120a4d0
e5c9cd9
94a0719
016a4fa
9441838
f686ed6
4ab8dc3
ceab4f8
f7adf51
b3e9330
ab368c4
ca0fe5b
244311b
efce104
ef36d82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -369,7 +369,7 @@ protected void onChildSourceInfoRefreshed( | |
| .handleSourceInfoRefresh(newTimeline); | ||
| maybeUpdateSourceInfo(); | ||
| } else { | ||
| checkArgument(newTimeline.getPeriodCount() == 1); | ||
| // checkArgument(newTimeline.getPeriodCount() == 1); | ||
| contentTimeline = newTimeline; | ||
| mainHandler.post( | ||
| () -> { | ||
|
|
@@ -510,7 +510,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)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed to |
||
| } else { | ||
| refreshSourceInfo(new MultiPeriodAdTimeline(contentTimeline, adPlaybackState)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /* | ||
| * 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> 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(); | ||
| Assertions.checkState(contentTimeline.getWindowCount() == 1); | ||
| this.adPlaybackStates = new AdPlaybackState[periodCount]; | ||
|
|
||
| final Timeline.Period period = new Timeline.Period(); | ||
| for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { | ||
| timeline.getPeriod(periodIndex, period); | ||
| adPlaybackStates[periodIndex] = forPeriod(adPlaybackState, period.positionInWindowUs, | ||
| periodIndex == periodCount - 1); | ||
| } | ||
| } | ||
|
|
||
| @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 isLastPeriod true if this is the last period | ||
| * @return adPlaybackState modified for period | ||
| */ | ||
| private AdPlaybackState forPeriod( | ||
| AdPlaybackState adPlaybackState, | ||
| long periodStartOffsetUs, | ||
| boolean isLastPeriod) { | ||
| 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 { | ||
| // 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This specific condition about the having the same number of periods is probably too restrictive overall (imagine using the AdsMediaSource in a playlist with other items).
Is it possible to check for
Timeline.Period.isPlaceholderin this condition to not apply this check when we go from placeholder to non-placeholder? This would of course only work if theisPlaceholderflag is changing at the right moment too. Alternatively, I wonder if we need additional checks for this logic to verify that the time of the ad group hasn't changed or is within the period duration. As you can see from the comment, the purpose of this check has really nothing to do with the multi-period ads, but with failed future ads that are not supposed to interrupt playback immediately.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to think about this. I implemented more specific check:
If the cuepoint being removed is not part of the period anymore, need to trigger update.
The update may not be postponed as the cuepoint will not be reached before the period ends.
Note: I am bit hesitant about using
periodbeing mutable in/out argument. Are there any best practices for using this?