From 22c58d9887dac3c05ff0ecfc0ab9b5bfec1e2add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kotula?= Date: Mon, 2 Jun 2025 11:44:48 +0200 Subject: [PATCH 1/7] Ads on multi period content --- .../exoplayer/source/ads/AdsMediaSource.java | 8 +- .../source/ads/MultiPeriodAdTimeline.java | 98 +++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java index 64e77da0f7f..27914441af5 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java @@ -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( () -> { @@ -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)); + } } } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java new file mode 100644 index 00000000000..5b883f75c38 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java @@ -0,0 +1,98 @@ +/* + * 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.Timeline; +import androidx.media3.common.util.Assertions; +import androidx.media3.exoplayer.source.ForwardingTimeline; + + +/** + * A custom {@link Timeline} for sources that have AdPlaybackState split among multiple periods. + */ +@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); + 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) + * @return adPlaybackState modified for period + */ + private AdPlaybackState forPeriod( + AdPlaybackState adPlaybackState, + long periodStartOffsetUs, + long periodDurationUs + ) { + final long periodEndUs = periodStartOffsetUs + periodDurationUs; + for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { + final long adGroupTimeUs = adPlaybackState.getAdGroup(adGroupIndex).timeUs; + 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; + } + +} From 67754051b8bd1c618a15a312d7e16ac45bb2e50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kotula?= Date: Mon, 2 Jun 2025 13:48:54 +0200 Subject: [PATCH 2/7] Test MultiPeriodAdTimeline --- .../source/ads/MultiPeriodAdTimelineTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java new file mode 100644 index 00000000000..f7b6279defe --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java @@ -0,0 +1,79 @@ +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.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)); + assertEquals(2, period1.getAdGroupIndexAfterPositionUs(1 * MICROS_PER_SECOND)); + assertEquals(2, period1.getAdGroupIndexForPositionUs(10 * MICROS_PER_SECOND)); + assertEquals(3, period1.getAdGroupIndexAfterPositionUs(10 * MICROS_PER_SECOND)); + assertEquals(3, period1.getAdGroupIndexForPositionUs(19 * MICROS_PER_SECOND)); + // no more ads to be played in 1st period + assertEquals(INDEX_UNSET, period1.getAdGroupIndexAfterPositionUs(19 * MICROS_PER_SECOND)); + + Period period2 = new Period(); + multiPeriodAdTimeline.getPeriod(2, period2); + + // positions within the 2nd period + assertEquals(3, period2.getAdGroupIndexForPositionUs(1 * MICROS_PER_SECOND)); + assertEquals(4, period2.getAdGroupIndexAfterPositionUs(1 * MICROS_PER_SECOND)); + assertEquals(4, period2.getAdGroupIndexForPositionUs(10 * MICROS_PER_SECOND)); + assertEquals(5, period2.getAdGroupIndexAfterPositionUs(10 * MICROS_PER_SECOND)); + assertEquals(5, period2.getAdGroupIndexForPositionUs(19 * MICROS_PER_SECOND)); + // no more ads to be played in 2nd period + assertEquals(INDEX_UNSET, period2.getAdGroupIndexAfterPositionUs(19 * MICROS_PER_SECOND)); + } +} From 1c14b2cfc47c05eeb57d1b5c76b4c5060e12699c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kotula?= Date: Mon, 2 Jun 2025 13:52:50 +0200 Subject: [PATCH 3/7] Add multi period DASH sample with ads --- demos/main/src/main/assets/media.exolist.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index fd9ea623245..bfbc60cb3d2 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -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", From eb0467b31e9f25625a57090f7b4fc76caf29887b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kotula?= Date: Tue, 3 Jun 2025 10:52:38 +0200 Subject: [PATCH 4/7] Special case for post-roll ads --- .../source/ads/MultiPeriodAdTimeline.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java index 5b883f75c38..35c52303210 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java @@ -17,6 +17,7 @@ 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; @@ -50,7 +51,7 @@ public MultiPeriodAdTimeline(Timeline contentTimeline, AdPlaybackState adPlaybac timeline.getPeriod(periodIndex, period); final long periodDurationUs = period.durationUs; adPlaybackStates[periodIndex] = forPeriod(adPlaybackState, periodStartOffsetUs, - periodDurationUs); + periodDurationUs, periodIndex == periodCount - 1); periodStartOffsetUs += periodDurationUs; } } @@ -74,23 +75,30 @@ public Period getPeriod(int periodIndex, Period period, boolean setIds) { * @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 isLast true if this is the last period * @return adPlaybackState modified for period */ private AdPlaybackState forPeriod( AdPlaybackState adPlaybackState, long periodStartOffsetUs, - long periodDurationUs - ) { + long periodDurationUs, + boolean isLast) { final long periodEndUs = periodStartOffsetUs + periodDurationUs; for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { final long adGroupTimeUs = adPlaybackState.getAdGroup(adGroupIndex).timeUs; - if (periodEndUs < adGroupTimeUs) { - // this cue point belongs to next periods - adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); + if (adGroupTimeUs == C.TIME_END_OF_SOURCE) { + if (!isLast) { + 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); } - // start time relative to period start - adPlaybackState = adPlaybackState.withAdGroupTimeUs(adGroupIndex, - adGroupTimeUs - periodStartOffsetUs); } return adPlaybackState; } From e38f026365099dfc3519debb9423bb2a2b6bc2a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kotula?= Date: Tue, 3 Jun 2025 12:52:03 +0200 Subject: [PATCH 5/7] Post-roll ads test --- .../source/ads/MultiPeriodAdTimelineTest.java | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java index f7b6279defe..51b727725e8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimelineTest.java @@ -2,6 +2,7 @@ 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; @@ -56,24 +57,68 @@ contentTimeline, new AdPlaybackState( multiPeriodAdTimeline.getPeriod(1, period1); // positions within the 1st period - assertEquals(1, period1.getAdGroupIndexForPositionUs(1 * MICROS_PER_SECOND)); - assertEquals(2, period1.getAdGroupIndexAfterPositionUs(1 * MICROS_PER_SECOND)); - assertEquals(2, period1.getAdGroupIndexForPositionUs(10 * MICROS_PER_SECOND)); - assertEquals(3, period1.getAdGroupIndexAfterPositionUs(10 * MICROS_PER_SECOND)); - assertEquals(3, period1.getAdGroupIndexForPositionUs(19 * MICROS_PER_SECOND)); + 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)); + 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)); - assertEquals(4, period2.getAdGroupIndexAfterPositionUs(1 * MICROS_PER_SECOND)); - assertEquals(4, period2.getAdGroupIndexForPositionUs(10 * MICROS_PER_SECOND)); - assertEquals(5, period2.getAdGroupIndexAfterPositionUs(10 * MICROS_PER_SECOND)); - assertEquals(5, period2.getAdGroupIndexForPositionUs(19 * MICROS_PER_SECOND)); + 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)); + 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 } } From ce4464c394ed3a914e81d1d114124d97c393d1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kotula?= Date: Wed, 4 Jun 2025 08:47:55 +0200 Subject: [PATCH 6/7] Rename parameter --- .../media3/exoplayer/source/ads/MultiPeriodAdTimeline.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java index 35c52303210..b0cb49864fd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java @@ -75,19 +75,19 @@ public Period getPeriod(int periodIndex, Period period, boolean setIds) { * @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 isLast true if this is the last period + * @param isLastPeriod true if this is the last period * @return adPlaybackState modified for period */ private AdPlaybackState forPeriod( AdPlaybackState adPlaybackState, long periodStartOffsetUs, long periodDurationUs, - boolean isLast) { + 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 (!isLast) { + if (!isLastPeriod) { adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); } } else { From 2ba33bc0a05674e5b33c9b0600f62e3dd85416bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kotula?= Date: Wed, 4 Jun 2025 09:01:19 +0200 Subject: [PATCH 7/7] Add java doc --- .../exoplayer/source/ads/MultiPeriodAdTimeline.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java index b0cb49864fd..88f1caa78bb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/MultiPeriodAdTimeline.java @@ -24,7 +24,15 @@ /** - * A custom {@link Timeline} for sources that have AdPlaybackState split among multiple periods. + * A custom {@link Timeline} for sources that have {@link AdPlaybackState} split among multiple periods. + *
+ * For each period a modified {@link AdPlaybackState} is created for each period: + *
    + *
  • ad group time is offset relative to period start time
  • + *
  • ad groups after period end time are marked as skipped
  • + *
  • post-roll ad group is kept only for last period
  • + *
  • ad group count and indices are kept unchanged
  • + *
*/ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public final class MultiPeriodAdTimeline extends ForwardingTimeline {