Skip to content

Commit 593c6fa

Browse files
marcbaechingercopybara-github
authored andcommitted
Map live interstitials to ad playback state
After this change, updates of the live HLS playlists are reflected in the ad playback state. Interstitials are inserted into new or existing ad groups according to the current ad playback state. PiperOrigin-RevId: 735733207
1 parent ecac78f commit 593c6fa

File tree

2 files changed

+780
-144
lines changed

2 files changed

+780
-144
lines changed

libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java

Lines changed: 172 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
*/
1616
package androidx.media3.exoplayer.hls;
1717

18+
import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE;
1819
import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
1920
import static androidx.media3.common.util.Assertions.checkArgument;
2021
import static androidx.media3.common.util.Assertions.checkNotNull;
2122
import static androidx.media3.common.util.Assertions.checkState;
2223
import static androidx.media3.common.util.Assertions.checkStateNotNull;
24+
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_POST;
25+
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_PRE;
2326
import static java.lang.Math.max;
2427

2528
import android.content.Context;
@@ -48,6 +51,7 @@
4851
import androidx.media3.exoplayer.source.ads.AdsLoader;
4952
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
5053
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
54+
import com.google.common.collect.ImmutableList;
5155
import com.google.errorprone.annotations.CanIgnoreReturnValue;
5256
import java.io.IOException;
5357
import java.util.ArrayList;
@@ -283,6 +287,7 @@ default void onStop(MediaItem mediaItem, Object adsId, AdPlaybackState adPlaybac
283287
private final PlayerListener playerListener;
284288
private final Map<Object, EventListener> activeEventListeners;
285289
private final Map<Object, AdPlaybackState> activeAdPlaybackStates;
290+
private final Map<Object, Set<String>> insertedInterstitialIds;
286291
private final List<Listener> listeners;
287292
private final Set<Object> unsupportedAdsIds;
288293

@@ -294,6 +299,7 @@ public HlsInterstitialsAdsLoader() {
294299
playerListener = new PlayerListener();
295300
activeEventListeners = new HashMap<>();
296301
activeAdPlaybackStates = new HashMap<>();
302+
insertedInterstitialIds = new HashMap<>();
297303
listeners = new ArrayList<>();
298304
unsupportedAdsIds = new HashSet<>();
299305
}
@@ -366,16 +372,15 @@ public void start(
366372
}
367373
activeEventListeners.put(adsId, eventListener);
368374
MediaItem mediaItem = adsMediaSource.getMediaItem();
369-
if (player != null && isSupportedMediaItem(mediaItem, player.getCurrentTimeline())) {
375+
if (isHlsMediaItem(mediaItem)) {
370376
// Mark with NONE. Update and notify later when timeline with interstitials arrives.
371377
activeAdPlaybackStates.put(adsId, AdPlaybackState.NONE);
378+
insertedInterstitialIds.put(adsId, new HashSet<>());
372379
notifyListeners(listener -> listener.onStart(mediaItem, adsId, adViewProvider));
373380
} else {
374381
putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId));
375-
if (player != null) {
376-
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
377-
unsupportedAdsIds.add(adsId);
378-
}
382+
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
383+
unsupportedAdsIds.add(adsId);
379384
}
380385
}
381386

@@ -387,24 +392,43 @@ public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline
387392
if (eventListener != null) {
388393
unsupportedAdsIds.remove(adsId);
389394
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.remove(adsId));
395+
insertedInterstitialIds.remove(adsId);
390396
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
391397
// Play without ads after release to not interrupt playback.
392398
eventListener.onAdPlaybackState(new AdPlaybackState(adsId));
393399
}
394400
}
395401
return;
396402
}
403+
397404
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.get(adsId));
398-
if (!adPlaybackState.equals(AdPlaybackState.NONE)) {
399-
// VOD only. Updating the playback state is not supported yet.
405+
if (!adPlaybackState.equals(AdPlaybackState.NONE)
406+
&& !adPlaybackState.endsWithLivePostrollPlaceHolder()) {
407+
// Multiple timeline updates for VOD not supported.
400408
return;
401409
}
402-
adPlaybackState = new AdPlaybackState(adsId);
410+
411+
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
412+
// Setup initial ad playback state for VOD or live.
413+
adPlaybackState = new AdPlaybackState(adsId);
414+
if (isLiveMediaItem(adsMediaSource.getMediaItem(), timeline)) {
415+
adPlaybackState =
416+
adPlaybackState.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false);
417+
}
418+
}
419+
403420
Window window = timeline.getWindow(0, new Window());
404421
if (window.manifest instanceof HlsManifest) {
422+
HlsMediaPlaylist mediaPlaylist = ((HlsManifest) window.manifest).mediaPlaylist;
405423
adPlaybackState =
406-
mapHlsInterstitialsToAdPlaybackState(
407-
((HlsManifest) window.manifest).mediaPlaylist, adPlaybackState);
424+
window.isLive()
425+
? mapInterstitialsForLive(
426+
mediaPlaylist,
427+
adPlaybackState,
428+
window.positionInFirstPeriodUs,
429+
checkNotNull(insertedInterstitialIds.get(adsId)))
430+
: mapInterstitialsForVod(
431+
mediaPlaylist, adPlaybackState, checkNotNull(insertedInterstitialIds.get(adsId)));
408432
}
409433
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
410434
if (!unsupportedAdsIds.contains(adsId)) {
@@ -464,6 +488,7 @@ public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) {
464488
adsMediaSource.getAdsId(),
465489
checkNotNull(adPlaybackState)));
466490
}
491+
insertedInterstitialIds.remove(adsId);
467492
unsupportedAdsIds.remove(adsId);
468493
}
469494

@@ -488,6 +513,7 @@ private void putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adP
488513
eventListener.onAdPlaybackState(adPlaybackState);
489514
} else {
490515
activeAdPlaybackStates.remove(adsId);
516+
insertedInterstitialIds.remove(adsId);
491517
}
492518
}
493519
}
@@ -498,10 +524,6 @@ private void notifyListeners(Consumer<Listener> callable) {
498524
}
499525
}
500526

501-
private static boolean isSupportedMediaItem(MediaItem mediaItem, Timeline timeline) {
502-
return isHlsMediaItem(mediaItem) && !isLiveMediaItem(mediaItem, timeline);
503-
}
504-
505527
private static boolean isLiveMediaItem(MediaItem mediaItem, Timeline timeline) {
506528
int windowIndex = timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
507529
Window window = new Window();
@@ -523,68 +545,161 @@ private static boolean isHlsMediaItem(MediaItem mediaItem) {
523545
|| Util.inferContentType(localConfiguration.uri) == C.CONTENT_TYPE_HLS;
524546
}
525547

526-
private static AdPlaybackState mapHlsInterstitialsToAdPlaybackState(
527-
HlsMediaPlaylist hlsMediaPlaylist, AdPlaybackState adPlaybackState) {
528-
for (int i = 0; i < hlsMediaPlaylist.interstitials.size(); i++) {
529-
Interstitial interstitial = hlsMediaPlaylist.interstitials.get(i);
548+
private static AdPlaybackState mapInterstitialsForLive(
549+
HlsMediaPlaylist mediaPlaylist,
550+
AdPlaybackState adPlaybackState,
551+
long windowPositionInPeriodUs,
552+
Set<String> insertedInterstitialIds) {
553+
ArrayList<Interstitial> interstitials = new ArrayList<>(mediaPlaylist.interstitials);
554+
for (int i = 0; i < interstitials.size(); i++) {
555+
Interstitial interstitial = interstitials.get(i);
556+
long positionInPlaylistWindowUs =
557+
interstitial.cue.contains(CUE_TRIGGER_PRE)
558+
? 0L
559+
: (interstitial.startDateUnixUs - mediaPlaylist.startTimeUs);
560+
if (interstitial.assetUri == null
561+
|| insertedInterstitialIds.contains(interstitial.id)
562+
|| interstitial.cue.contains(CUE_TRIGGER_POST)
563+
|| positionInPlaylistWindowUs < 0) {
564+
continue;
565+
}
566+
long timeUs = windowPositionInPeriodUs + positionInPlaylistWindowUs;
567+
int insertionIndex = adPlaybackState.adGroupCount - 1;
568+
boolean isNewAdGroup = true;
569+
for (int adGroupIndex = adPlaybackState.adGroupCount - 2; // skip live placeholder
570+
adGroupIndex >= adPlaybackState.removedAdGroupCount;
571+
adGroupIndex--) {
572+
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
573+
if (adGroup.timeUs == timeUs) {
574+
// Insert interstitials into or update in existing group.
575+
insertionIndex = adGroupIndex;
576+
isNewAdGroup = false;
577+
break;
578+
} else if (adGroup.timeUs < timeUs) {
579+
// Insert at index after group before interstitial.
580+
insertionIndex = adGroupIndex + 1;
581+
break;
582+
}
583+
// Interstitial is before the ad group. Possible insertion index.
584+
insertionIndex = adGroupIndex;
585+
}
586+
if (isNewAdGroup) {
587+
if (insertionIndex < getLowestValidAdGroupInsertionIndex(adPlaybackState)) {
588+
Log.w(
589+
TAG,
590+
"Skipping insertion of interstitial attempted to be inserted before an already"
591+
+ " initialized ad group.");
592+
continue;
593+
}
594+
adPlaybackState = adPlaybackState.withNewAdGroup(insertionIndex, timeUs);
595+
}
596+
adPlaybackState =
597+
insertOrUpdateInterstitialInAdGroup(
598+
interstitial, /* adGroupIndex= */ insertionIndex, adPlaybackState);
599+
insertedInterstitialIds.add(interstitial.id);
600+
}
601+
return adPlaybackState;
602+
}
603+
604+
private static AdPlaybackState mapInterstitialsForVod(
605+
HlsMediaPlaylist mediaPlaylist,
606+
AdPlaybackState adPlaybackState,
607+
Set<String> insertedInterstitialIds) {
608+
checkArgument(adPlaybackState.adGroupCount == 0);
609+
ImmutableList<Interstitial> interstitials = mediaPlaylist.interstitials;
610+
for (int i = 0; i < interstitials.size(); i++) {
611+
Interstitial interstitial = interstitials.get(i);
530612
if (interstitial.assetUri == null) {
531613
Log.w(TAG, "Ignoring interstitials with X-ASSET-LIST. Not yet supported.");
532614
continue;
533615
}
534-
long positionUs;
535-
if (interstitial.cue.contains(Interstitial.CUE_TRIGGER_PRE)) {
536-
positionUs = 0;
537-
} else if (interstitial.cue.contains(Interstitial.CUE_TRIGGER_POST)) {
538-
positionUs = C.TIME_END_OF_SOURCE;
616+
long timeUs;
617+
if (interstitial.cue.contains(CUE_TRIGGER_PRE)) {
618+
timeUs = 0L;
619+
} else if (interstitial.cue.contains(CUE_TRIGGER_POST)) {
620+
timeUs = C.TIME_END_OF_SOURCE;
539621
} else {
540-
positionUs = interstitial.startDateUnixUs - hlsMediaPlaylist.startTimeUs;
622+
timeUs = interstitial.startDateUnixUs - mediaPlaylist.startTimeUs;
541623
}
542-
// Check whether and at which index to insert an ad group for the interstitial start time.
543624
int adGroupIndex =
544-
adPlaybackState.getAdGroupIndexForPositionUs(
545-
positionUs, /* periodDurationUs= */ hlsMediaPlaylist.durationUs);
625+
adPlaybackState.getAdGroupIndexForPositionUs(timeUs, mediaPlaylist.durationUs);
546626
if (adGroupIndex == C.INDEX_UNSET) {
547627
// There is no ad group before or at the interstitials position.
548628
adGroupIndex = 0;
549-
adPlaybackState = adPlaybackState.withNewAdGroup(0, positionUs);
550-
} else if (adPlaybackState.getAdGroup(adGroupIndex).timeUs != positionUs) {
629+
adPlaybackState = adPlaybackState.withNewAdGroup(/* adGroupIndex= */ 0, timeUs);
630+
} else if (adPlaybackState.getAdGroup(adGroupIndex).timeUs != timeUs) {
551631
// There is an ad group before the interstitials. Insert after that index.
552632
adGroupIndex++;
553-
adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, positionUs);
633+
adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, timeUs);
554634
}
635+
adPlaybackState =
636+
insertOrUpdateInterstitialInAdGroup(interstitial, adGroupIndex, adPlaybackState);
637+
insertedInterstitialIds.add(interstitial.id);
638+
}
639+
return adPlaybackState;
640+
}
555641

556-
int adIndexInAdGroup = max(adPlaybackState.getAdGroup(adGroupIndex).count, 0);
557-
558-
// Insert duration of new interstitial into existing ad durations.
559-
long interstitialDurationUs =
560-
getInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET);
561-
long[] adDurations;
562-
if (adIndexInAdGroup == 0) {
563-
adDurations = new long[1];
564-
} else {
565-
long[] previousDurations = adPlaybackState.getAdGroup(adGroupIndex).durationsUs;
566-
adDurations = new long[previousDurations.length + 1];
567-
System.arraycopy(previousDurations, 0, adDurations, 0, previousDurations.length);
568-
}
569-
adDurations[adDurations.length - 1] = interstitialDurationUs;
570-
571-
long resumeOffsetIncrementUs =
572-
interstitial.resumeOffsetUs != C.TIME_UNSET
573-
? interstitial.resumeOffsetUs
574-
: (interstitialDurationUs != C.TIME_UNSET ? interstitialDurationUs : 0L);
575-
long resumeOffsetUs =
576-
adPlaybackState.getAdGroup(adGroupIndex).contentResumeOffsetUs + resumeOffsetIncrementUs;
642+
private static AdPlaybackState insertOrUpdateInterstitialInAdGroup(
643+
Interstitial interstitial, int adGroupIndex, AdPlaybackState adPlaybackState) {
644+
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
645+
int adIndexInAdGroup = adGroup.getIndexOfAdId(interstitial.id);
646+
if (adIndexInAdGroup != C.INDEX_UNSET) {
647+
// Interstitial already inserted. Updating not yet supported.
648+
return adPlaybackState;
649+
}
650+
651+
// Append to the end of the group.
652+
adIndexInAdGroup = max(adGroup.count, 0);
653+
// Append duration of new interstitial into existing ad durations.
654+
long interstitialDurationUs =
655+
getInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET);
656+
long[] adDurations;
657+
if (adIndexInAdGroup == 0) {
658+
adDurations = new long[1];
659+
} else {
660+
long[] previousDurations = adGroup.durationsUs;
661+
adDurations = new long[previousDurations.length + 1];
662+
System.arraycopy(previousDurations, 0, adDurations, 0, previousDurations.length);
663+
}
664+
adDurations[adDurations.length - 1] = interstitialDurationUs;
665+
long resumeOffsetIncrementUs =
666+
interstitial.resumeOffsetUs != C.TIME_UNSET
667+
? interstitial.resumeOffsetUs
668+
: (interstitialDurationUs != C.TIME_UNSET ? interstitialDurationUs : 0L);
669+
long resumeOffsetUs = adGroup.contentResumeOffsetUs + resumeOffsetIncrementUs;
670+
adPlaybackState =
671+
adPlaybackState
672+
.withAdCount(adGroupIndex, adIndexInAdGroup + 1)
673+
.withAdId(adGroupIndex, adIndexInAdGroup, interstitial.id)
674+
.withAdDurationsUs(adGroupIndex, adDurations)
675+
.withContentResumeOffsetUs(adGroupIndex, resumeOffsetUs);
676+
if (interstitial.assetUri != null) {
577677
adPlaybackState =
578-
adPlaybackState
579-
.withAdCount(adGroupIndex, /* adCount= */ adIndexInAdGroup + 1)
580-
.withAdDurationsUs(adGroupIndex, adDurations)
581-
.withContentResumeOffsetUs(adGroupIndex, resumeOffsetUs)
582-
.withAvailableAdMediaItem(
583-
adGroupIndex, adIndexInAdGroup, MediaItem.fromUri(interstitial.assetUri));
678+
adPlaybackState.withAvailableAdMediaItem(
679+
adGroupIndex,
680+
adIndexInAdGroup,
681+
new MediaItem.Builder()
682+
.setUri(interstitial.assetUri)
683+
.setMimeType(MimeTypes.APPLICATION_M3U8)
684+
.build());
584685
}
585686
return adPlaybackState;
586687
}
587688

689+
private static int getLowestValidAdGroupInsertionIndex(AdPlaybackState adPlaybackState) {
690+
for (int adGroupIndex = adPlaybackState.adGroupCount - 1;
691+
adGroupIndex >= adPlaybackState.removedAdGroupCount;
692+
adGroupIndex--) {
693+
for (@AdPlaybackState.AdState int state : adPlaybackState.getAdGroup(adGroupIndex).states) {
694+
if (state != AD_STATE_UNAVAILABLE) {
695+
return adGroupIndex + 1;
696+
}
697+
}
698+
}
699+
// All ad groups unavailable.
700+
return adPlaybackState.removedAdGroupCount;
701+
}
702+
588703
private static long getInterstitialDurationUs(Interstitial interstitial, long defaultDurationUs) {
589704
if (interstitial.playoutLimitUs != C.TIME_UNSET) {
590705
return interstitial.playoutLimitUs;

0 commit comments

Comments
 (0)