Skip to content

Commit bf67d1c

Browse files
toniheimicrokatz
authored andcommitted
Calculate SSAI window duration for live periods with unset duration.
We currently skip this calculation entirely, but it can be added by calculating the window duration using the wrapped window's duration and the provided AdPlaybackState. Issue: google/ExoPlayer#10764 PiperOrigin-RevId: 488614767 (cherry picked from commit 7a7d083)
1 parent e4ff4a3 commit bf67d1c

File tree

3 files changed

+101
-8
lines changed

3 files changed

+101
-8
lines changed

RELEASENOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ Release notes
113113
([#10510](https://github.com/google/ExoPlayer/issues/10510)).
114114
* Prevent skipping mid-roll ads when seeking to the end of the content
115115
([#10685](https://github.com/google/ExoPlayer/issues/10685)).
116+
* Correctly calculate window duration for live streams with server-side
117+
inserted ads, for example IMA DAI
118+
([#10764](https://github.com/google/ExoPlayer/issues/10764)).
116119
* FFmpeg extension:
117120
* Add newly required flags to link FFmpeg libraries with NDK 23.1.7779620
118121
and above ([#9933](https://github.com/google/ExoPlayer/issues/9933)).

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,8 +1019,9 @@ public ServerSideAdInsertionTimeline(
10191019
@Override
10201020
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
10211021
super.getWindow(windowIndex, window, defaultPositionProjectionUs);
1022+
Period period = new Period();
10221023
Object firstPeriodUid =
1023-
checkNotNull(getPeriod(window.firstPeriodIndex, new Period(), /* setIds= */ true).uid);
1024+
checkNotNull(getPeriod(window.firstPeriodIndex, period, /* setIds= */ true).uid);
10241025
AdPlaybackState firstAdPlaybackState = checkNotNull(adPlaybackStates.get(firstPeriodUid));
10251026
long positionInPeriodUs =
10261027
getMediaPeriodPositionUsForContent(
@@ -1032,11 +1033,21 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj
10321033
window.durationUs = firstAdPlaybackState.contentDurationUs - positionInPeriodUs;
10331034
}
10341035
} else {
1035-
Period lastPeriod = getPeriod(/* periodIndex= */ window.lastPeriodIndex, new Period());
1036+
Period originalLastPeriod =
1037+
super.getPeriod(/* periodIndex= */ window.lastPeriodIndex, period, /* setIds= */ true);
1038+
long originalLastPeriodPositionInWindowUs = originalLastPeriod.positionInWindowUs;
1039+
AdPlaybackState lastAdPlaybackState =
1040+
checkNotNull(adPlaybackStates.get(originalLastPeriod.uid));
1041+
Period adjustedLastPeriod = getPeriod(/* periodIndex= */ window.lastPeriodIndex, period);
1042+
long originalWindowDurationInLastPeriodUs =
1043+
window.durationUs - originalLastPeriodPositionInWindowUs;
1044+
long adjustedWindowDurationInLastPeriodUs =
1045+
getMediaPeriodPositionUsForContent(
1046+
originalWindowDurationInLastPeriodUs,
1047+
/* nextAdGroupIndex= */ C.INDEX_UNSET,
1048+
lastAdPlaybackState);
10361049
window.durationUs =
1037-
lastPeriod.durationUs == C.TIME_UNSET
1038-
? C.TIME_UNSET
1039-
: lastPeriod.positionInWindowUs + lastPeriod.durationUs;
1050+
adjustedLastPeriod.positionInWindowUs + adjustedWindowDurationInLastPeriodUs;
10401051
}
10411052
window.positionInFirstPeriodUs = positionInPeriodUs;
10421053
return window;

libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSourceTest.java

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@
3636
import android.util.Pair;
3737
import android.view.Surface;
3838
import androidx.media3.common.AdPlaybackState;
39+
import androidx.media3.common.C;
3940
import androidx.media3.common.MediaItem;
4041
import androidx.media3.common.Player;
4142
import androidx.media3.common.Timeline;
4243
import androidx.media3.exoplayer.ExoPlayer;
4344
import androidx.media3.exoplayer.analytics.AnalyticsListener;
4445
import androidx.media3.exoplayer.analytics.PlayerId;
4546
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
47+
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
4648
import androidx.media3.test.utils.CapturingRenderersFactory;
4749
import androidx.media3.test.utils.DumpFileAsserts;
4850
import androidx.media3.test.utils.FakeClock;
@@ -71,15 +73,15 @@ public final class ServerSideAdInsertionMediaSourceTest {
7173
private static final String TEST_ASSET_DUMP = "playbackdumps/mp4/ssai-sample.mp4.dump";
7274

7375
@Test
74-
public void timeline_containsAdsDefinedInAdPlaybackState() throws Exception {
76+
public void timeline_vodSinglePeriod_containsAdsDefinedInAdPlaybackState() throws Exception {
7577
FakeTimeline wrappedTimeline =
7678
new FakeTimeline(
7779
new FakeTimeline.TimelineWindowDefinition(
7880
/* periodCount= */ 1,
7981
/* id= */ 0,
8082
/* isSeekable= */ true,
81-
/* isDynamic= */ true,
82-
/* isLive= */ true,
83+
/* isDynamic= */ false,
84+
/* isLive= */ false,
8385
/* isPlaceholder= */ false,
8486
/* durationUs= */ 10_000_000,
8587
/* defaultPositionUs= */ 3_000_000,
@@ -147,6 +149,83 @@ public void timeline_containsAdsDefinedInAdPlaybackState() throws Exception {
147149
assertThat(window.durationUs).isEqualTo(9_800_000);
148150
}
149151

152+
@Test
153+
public void timeline_liveSinglePeriodWithUnsetPeriodDuration_containsAdsDefinedInAdPlaybackState()
154+
throws Exception {
155+
Timeline wrappedTimeline =
156+
new SinglePeriodTimeline(
157+
/* periodDurationUs= */ C.TIME_UNSET,
158+
/* windowDurationUs= */ 10_000_000,
159+
/* windowPositionInPeriodUs= */ 42_000_000L,
160+
/* windowDefaultStartPositionUs= */ 3_000_000,
161+
/* isSeekable= */ true,
162+
/* isDynamic= */ true,
163+
/* useLiveConfiguration= */ true,
164+
/* manifest= */ null,
165+
/* mediaItem= */ MediaItem.EMPTY);
166+
ServerSideAdInsertionMediaSource mediaSource =
167+
new ServerSideAdInsertionMediaSource(
168+
new FakeMediaSource(wrappedTimeline), /* adPlaybackStateUpdater= */ null);
169+
// Test with one ad group before the window, and the window starting within the second ad group.
170+
AdPlaybackState adPlaybackState =
171+
new AdPlaybackState(
172+
/* adsId= */ new Object(), /* adGroupTimesUs= */ 15_000_000, 41_500_000, 42_200_000)
173+
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
174+
.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true)
175+
.withIsServerSideInserted(/* adGroupIndex= */ 2, /* isServerSideInserted= */ true)
176+
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
177+
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2)
178+
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
179+
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs= */ 500_000)
180+
.withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs= */ 300_000, 100_000)
181+
.withAdDurationsUs(/* adGroupIndex= */ 2, /* adDurationsUs= */ 400_000)
182+
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, /* contentResumeOffsetUs= */ 100_000)
183+
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000)
184+
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000);
185+
AtomicReference<Timeline> timelineReference = new AtomicReference<>();
186+
mediaSource.setAdPlaybackStates(
187+
ImmutableMap.of(
188+
wrappedTimeline.getPeriod(
189+
/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
190+
.uid,
191+
adPlaybackState));
192+
193+
mediaSource.prepareSource(
194+
(source, timeline) -> timelineReference.set(timeline),
195+
/* mediaTransferListener= */ null,
196+
PlayerId.UNSET);
197+
runMainLooperUntil(() -> timelineReference.get() != null);
198+
199+
Timeline timeline = timelineReference.get();
200+
assertThat(timeline.getPeriodCount()).isEqualTo(1);
201+
Timeline.Period period = timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period());
202+
assertThat(period.getAdGroupCount()).isEqualTo(3);
203+
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 0)).isEqualTo(1);
204+
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 1)).isEqualTo(2);
205+
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 2)).isEqualTo(1);
206+
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(15_000_000);
207+
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 1)).isEqualTo(41_500_000);
208+
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 2)).isEqualTo(42_200_000);
209+
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
210+
.isEqualTo(500_000);
211+
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0))
212+
.isEqualTo(300_000);
213+
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1))
214+
.isEqualTo(100_000);
215+
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0))
216+
.isEqualTo(400_000);
217+
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 0)).isEqualTo(100_000);
218+
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 1)).isEqualTo(400_000);
219+
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 2)).isEqualTo(200_000);
220+
assertThat(period.getDurationUs()).isEqualTo(C.TIME_UNSET);
221+
// positionInWindowUs + sum(adDurationsBeforeWindow) - sum(contentResumeOffsetsBeforeWindow)
222+
assertThat(period.getPositionInWindowUs()).isEqualTo(-41_600_000);
223+
Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
224+
assertThat(window.positionInFirstPeriodUs).isEqualTo(41_600_000);
225+
// windowDurationUs - sum(adDurationsInWindow) + sum(applicableContentResumeOffsetUs)
226+
assertThat(window.durationUs).isEqualTo(9_800_000);
227+
}
228+
150229
@Test
151230
public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() {
152231
ServerSideAdInsertionMediaSource mediaSource =

0 commit comments

Comments
 (0)