Skip to content

Multi period content support for AdsMediaSource#3126

Open
kotucz wants to merge 12 commits intoandroidx:mainfrom
kotucz:multi-period-ads
Open

Multi period content support for AdsMediaSource#3126
kotucz wants to merge 12 commits intoandroidx:mainfrom
kotucz:multi-period-ads

Conversation

@kotucz
Copy link

@kotucz kotucz commented Mar 19, 2026

Multi period DASH content ads support inspired by #1642 (comment)
Reopens: #2501

At Timeline level, the original AdPlaybackState is divided and adjusted for each of the periods.

Removing single period assertions from AdsMediaSource.
Providing MultiPeriodAdTimeline as an alternative to current SinglePeriodAdTimeline.
From input AdPlaybackState, new modified AdPlaybackState is created - one for each period:

More detail is also here:
https://blog.hotstar.com/monetisation-multi-period-dash-x-exoplayer-db8e8c00e521

We have encountered an issue, which is also handled in this PR:

Player is using MaskingMediaSource to reduce start lag. This means preroll ads are loaded in parallel to resolving content manifest. Meanwhile placeholder manifest is used for content temporarily having single period.

Ads are initialized as follows:

  • preroll - skipped (no fill, there is no preroll)
  • the rest of the midrolls are playable, but not in the period 0

After the content manifest (MPD) is resolved content will have multiple periods.
For each period we will create an adjusted copy the main AdPlaybackState to have specific config of ads.

Preroll will stay unchange (no fill), while midrolls for period 0 (bumper/disclaimer) will be marked as SKIPPED as these should not be played in this period.

Note: if preroll is available it refreshes the timeline when it is done playing at this moment real timeline with periods is available and AdPlaybackStates are reflected as expected

ExoPlayer has evaluates this update can be ignored. (Placeholder and real timeline have same Id and are not distinguished)
Player plays until the ad group is reached. But since the ad group is only some time after the period end, player is stuck in the end of period 0, waiting for the midroll to be played.

Note: Seekbar or fast forward button can be used to unstuck the player (forcing Timeline update).

FIX: Player timeline will be recreated when period count is changed

Example:

-------------- placeholder period -------------------

|      period 0          |           period 1                                          ...          | end of content
0s                       5s                                         15s
preroll(empty)                                                     midroll

What happens:

  • playback period 0 till its end 5s
  • stuck for 10 sec in the end of period 0
  • play midroll at 15s
  • play period 1

Midroll is not in period 0, but this change is not reflected in the Timeline on the placeholder timeline -> multiperiod timeline transition

periodIdWithAds.nextAdGroupIndex -1
oldPeriodId.nextAdGroupIndex 1

@tonihei tonihei self-assigned this Mar 20, 2026
@tonihei
Copy link
Collaborator

tonihei commented Mar 20, 2026

Thanks for the pull request (and rebasing your earlier attempt from #2501)! I'll leave some comment for discussion, but overall looks like the right approach.

boolean sameOldAndNewPeriodUid = oldPeriodId.periodUid.equals(newPeriodUid);
boolean onlyNextAdGroupIndexIncreased =
sameOldAndNewPeriodUid
&& samePeriodCount
Copy link
Collaborator

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.isPlaceholder in this condition to not apply this check when we go from placeholder to non-placeholder? This would of course only work if the isPlaceholder flag 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.

adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs());
refreshSourceInfo(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
if (contentTimeline.getPeriodCount() == 1) {
refreshSourceInfo(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SinglePeriodAdTimeline now sounds like a subset of the MultiPeriodAdTimelime - could this just be changed to be simply an AdTimeline? We likely need to keep SinglePeriodAdTimeline as deprecated to avoid breakages, but otherwise it looks like the new class can be just be used instead of the old one.

* <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>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is surprising to me because it sounds like we would play all ads repeatedly for all periods. I assume some of the internal player logic prevents that from happening, but it would be much cleaner if we could actually split the AdPlaybackState fully into the individual ads for each period.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second note: When looking for other adjustments that might be needed in AdsMediaSource, I realized you avoided making these adjustments by keeping the adGroupIndex stable across all periods. So if you follow my suggestion here to split the AdPlaybackState properly, the logic in AdsMediaSource.createPeriod/releasePeriod/onChildSourceInfoRefreshed and potentially other places would need to change to reverse the mapping from adGroupIndex in a period to adGroupIndex in the 'global' AdPlaybackState.

Given all that, I think your approach of keeping the indices stable without further mapping is actually quite useful. I'm curious to hear whether you tried the other idea of splitting the state further still.

} else {
// start time relative to period start
adPlaybackState = adPlaybackState.withAdGroupTimeUs(adGroupIndex,
adGroupTimeUs - periodStartOffsetUs);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is negative or beyond the period duration, should it also be marked as "skipped" to more clearly signal that this ad group should not be played for this period?

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2017 The Android Open Source Project
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2026 :)

@tonihei
Copy link
Collaborator

tonihei commented Mar 20, 2026

Re-reading the discussion of #1642 (comment), I also wonder if you thought about adjustments needed in AdsLoader related classes like IMA's AdTagLoader. I see a few places that retrieve positions from the player or access AdPlaybackState information via the Timeline obtained from the Player object, which may now have the per-period adjustments. Did you see any issues with that or were you using your own custom AdsLoader?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants