Skip to content

Commit

Permalink
Calculate SSAI window duration for live periods with unset duration.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tonihei authored and microkatz committed Nov 16, 2022
1 parent 66dc6b3 commit 7a7d083
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 8 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Expand Up @@ -119,6 +119,9 @@ Release notes
([#10510](https://github.com/google/ExoPlayer/issues/10510)).
* Prevent skipping mid-roll ads when seeking to the end of the content
([#10685](https://github.com/google/ExoPlayer/issues/10685)).
* Correctly calculate window duration for live streams with server-side
inserted ads, for example IMA DAI
([#10764](https://github.com/google/ExoPlayer/issues/10764)).
* FFmpeg extension:
* Add newly required flags to link FFmpeg libraries with NDK 23.1.7779620
and above ([#9933](https://github.com/google/ExoPlayer/issues/9933)).
Expand Down
Expand Up @@ -1019,8 +1019,9 @@ public ServerSideAdInsertionTimeline(
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
super.getWindow(windowIndex, window, defaultPositionProjectionUs);
Period period = new Period();
Object firstPeriodUid =
checkNotNull(getPeriod(window.firstPeriodIndex, new Period(), /* setIds= */ true).uid);
checkNotNull(getPeriod(window.firstPeriodIndex, period, /* setIds= */ true).uid);
AdPlaybackState firstAdPlaybackState = checkNotNull(adPlaybackStates.get(firstPeriodUid));
long positionInPeriodUs =
getMediaPeriodPositionUsForContent(
Expand All @@ -1032,11 +1033,21 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj
window.durationUs = firstAdPlaybackState.contentDurationUs - positionInPeriodUs;
}
} else {
Period lastPeriod = getPeriod(/* periodIndex= */ window.lastPeriodIndex, new Period());
Period originalLastPeriod =
super.getPeriod(/* periodIndex= */ window.lastPeriodIndex, period, /* setIds= */ true);
long originalLastPeriodPositionInWindowUs = originalLastPeriod.positionInWindowUs;
AdPlaybackState lastAdPlaybackState =
checkNotNull(adPlaybackStates.get(originalLastPeriod.uid));
Period adjustedLastPeriod = getPeriod(/* periodIndex= */ window.lastPeriodIndex, period);
long originalWindowDurationInLastPeriodUs =
window.durationUs - originalLastPeriodPositionInWindowUs;
long adjustedWindowDurationInLastPeriodUs =
getMediaPeriodPositionUsForContent(
originalWindowDurationInLastPeriodUs,
/* nextAdGroupIndex= */ C.INDEX_UNSET,
lastAdPlaybackState);
window.durationUs =
lastPeriod.durationUs == C.TIME_UNSET
? C.TIME_UNSET
: lastPeriod.positionInWindowUs + lastPeriod.durationUs;
adjustedLastPeriod.positionInWindowUs + adjustedWindowDurationInLastPeriodUs;
}
window.positionInFirstPeriodUs = positionInPeriodUs;
return window;
Expand Down
Expand Up @@ -36,13 +36,15 @@
import android.util.Pair;
import android.view.Surface;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.test.utils.CapturingRenderersFactory;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.FakeClock;
Expand Down Expand Up @@ -71,15 +73,15 @@ public final class ServerSideAdInsertionMediaSourceTest {
private static final String TEST_ASSET_DUMP = "playbackdumps/mp4/ssai-sample.mp4.dump";

@Test
public void timeline_containsAdsDefinedInAdPlaybackState() throws Exception {
public void timeline_vodSinglePeriod_containsAdsDefinedInAdPlaybackState() throws Exception {
FakeTimeline wrappedTimeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 3_000_000,
Expand Down Expand Up @@ -144,6 +146,83 @@ public void timeline_containsAdsDefinedInAdPlaybackState() throws Exception {
assertThat(window.durationUs).isEqualTo(9_800_000);
}

@Test
public void timeline_liveSinglePeriodWithUnsetPeriodDuration_containsAdsDefinedInAdPlaybackState()
throws Exception {
Timeline wrappedTimeline =
new SinglePeriodTimeline(
/* periodDurationUs= */ C.TIME_UNSET,
/* windowDurationUs= */ 10_000_000,
/* windowPositionInPeriodUs= */ 42_000_000L,
/* windowDefaultStartPositionUs= */ 3_000_000,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* useLiveConfiguration= */ true,
/* manifest= */ null,
/* mediaItem= */ MediaItem.EMPTY);
ServerSideAdInsertionMediaSource mediaSource =
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(wrappedTimeline), /* adPlaybackStateUpdater= */ null);
// Test with one ad group before the window, and the window starting within the second ad group.
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ new Object(), /* adGroupTimesUs= */ 15_000_000, 41_500_000, 42_200_000)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true)
.withIsServerSideInserted(/* adGroupIndex= */ 2, /* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2)
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs= */ 500_000)
.withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs= */ 300_000, 100_000)
.withAdDurationsUs(/* adGroupIndex= */ 2, /* adDurationsUs= */ 400_000)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, /* contentResumeOffsetUs= */ 100_000)
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000)
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000);
AtomicReference<Timeline> timelineReference = new AtomicReference<>();
mediaSource.setAdPlaybackStates(
ImmutableMap.of(
wrappedTimeline.getPeriod(
/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
.uid,
adPlaybackState));

mediaSource.prepareSource(
(source, timeline) -> timelineReference.set(timeline),
/* mediaTransferListener= */ null,
PlayerId.UNSET);
runMainLooperUntil(() -> timelineReference.get() != null);

Timeline timeline = timelineReference.get();
assertThat(timeline.getPeriodCount()).isEqualTo(1);
Timeline.Period period = timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period());
assertThat(period.getAdGroupCount()).isEqualTo(3);
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 0)).isEqualTo(1);
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 1)).isEqualTo(2);
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 2)).isEqualTo(1);
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(15_000_000);
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 1)).isEqualTo(41_500_000);
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 2)).isEqualTo(42_200_000);
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
.isEqualTo(500_000);
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0))
.isEqualTo(300_000);
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1))
.isEqualTo(100_000);
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0))
.isEqualTo(400_000);
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 0)).isEqualTo(100_000);
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 1)).isEqualTo(400_000);
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 2)).isEqualTo(200_000);
assertThat(period.getDurationUs()).isEqualTo(C.TIME_UNSET);
// positionInWindowUs + sum(adDurationsBeforeWindow) - sum(contentResumeOffsetsBeforeWindow)
assertThat(period.getPositionInWindowUs()).isEqualTo(-41_600_000);
Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
assertThat(window.positionInFirstPeriodUs).isEqualTo(41_600_000);
// windowDurationUs - sum(adDurationsInWindow) + sum(applicableContentResumeOffsetUs)
assertThat(window.durationUs).isEqualTo(9_800_000);
}

@Test
public void timeline_missingAdPlaybackStateByPeriodUid_isAssertedAndThrows() {
ServerSideAdInsertionMediaSource mediaSource =
Expand Down

0 comments on commit 7a7d083

Please sign in to comment.