1515 */
1616package androidx .media3 .exoplayer .hls ;
1717
18+ import static androidx .media3 .common .AdPlaybackState .AD_STATE_UNAVAILABLE ;
1819import static androidx .media3 .common .Player .DISCONTINUITY_REASON_AUTO_TRANSITION ;
1920import static androidx .media3 .common .util .Assertions .checkArgument ;
2021import static androidx .media3 .common .util .Assertions .checkNotNull ;
2122import static androidx .media3 .common .util .Assertions .checkState ;
2223import 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 ;
2326import static java .lang .Math .max ;
2427
2528import android .content .Context ;
4851import androidx .media3 .exoplayer .source .ads .AdsLoader ;
4952import androidx .media3 .exoplayer .source .ads .AdsMediaSource ;
5053import androidx .media3 .exoplayer .upstream .LoadErrorHandlingPolicy ;
54+ import com .google .common .collect .ImmutableList ;
5155import com .google .errorprone .annotations .CanIgnoreReturnValue ;
5256import java .io .IOException ;
5357import 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