15
15
*/
16
16
package androidx .media3 .exoplayer .hls ;
17
17
18
+ import static androidx .media3 .common .AdPlaybackState .AD_STATE_UNAVAILABLE ;
18
19
import static androidx .media3 .common .Player .DISCONTINUITY_REASON_AUTO_TRANSITION ;
19
20
import static androidx .media3 .common .util .Assertions .checkArgument ;
20
21
import static androidx .media3 .common .util .Assertions .checkNotNull ;
21
22
import static androidx .media3 .common .util .Assertions .checkState ;
22
23
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 ;
23
26
import static java .lang .Math .max ;
24
27
25
28
import android .content .Context ;
48
51
import androidx .media3 .exoplayer .source .ads .AdsLoader ;
49
52
import androidx .media3 .exoplayer .source .ads .AdsMediaSource ;
50
53
import androidx .media3 .exoplayer .upstream .LoadErrorHandlingPolicy ;
54
+ import com .google .common .collect .ImmutableList ;
51
55
import com .google .errorprone .annotations .CanIgnoreReturnValue ;
52
56
import java .io .IOException ;
53
57
import java .util .ArrayList ;
@@ -283,6 +287,7 @@ default void onStop(MediaItem mediaItem, Object adsId, AdPlaybackState adPlaybac
283
287
private final PlayerListener playerListener ;
284
288
private final Map <Object , EventListener > activeEventListeners ;
285
289
private final Map <Object , AdPlaybackState > activeAdPlaybackStates ;
290
+ private final Map <Object , Set <String >> insertedInterstitialIds ;
286
291
private final List <Listener > listeners ;
287
292
private final Set <Object > unsupportedAdsIds ;
288
293
@@ -294,6 +299,7 @@ public HlsInterstitialsAdsLoader() {
294
299
playerListener = new PlayerListener ();
295
300
activeEventListeners = new HashMap <>();
296
301
activeAdPlaybackStates = new HashMap <>();
302
+ insertedInterstitialIds = new HashMap <>();
297
303
listeners = new ArrayList <>();
298
304
unsupportedAdsIds = new HashSet <>();
299
305
}
@@ -366,16 +372,15 @@ public void start(
366
372
}
367
373
activeEventListeners .put (adsId , eventListener );
368
374
MediaItem mediaItem = adsMediaSource .getMediaItem ();
369
- if (player != null && isSupportedMediaItem (mediaItem , player . getCurrentTimeline () )) {
375
+ if (isHlsMediaItem (mediaItem )) {
370
376
// Mark with NONE. Update and notify later when timeline with interstitials arrives.
371
377
activeAdPlaybackStates .put (adsId , AdPlaybackState .NONE );
378
+ insertedInterstitialIds .put (adsId , new HashSet <>());
372
379
notifyListeners (listener -> listener .onStart (mediaItem , adsId , adViewProvider ));
373
380
} else {
374
381
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 );
379
384
}
380
385
}
381
386
@@ -387,24 +392,43 @@ public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline
387
392
if (eventListener != null ) {
388
393
unsupportedAdsIds .remove (adsId );
389
394
AdPlaybackState adPlaybackState = checkNotNull (activeAdPlaybackStates .remove (adsId ));
395
+ insertedInterstitialIds .remove (adsId );
390
396
if (adPlaybackState .equals (AdPlaybackState .NONE )) {
391
397
// Play without ads after release to not interrupt playback.
392
398
eventListener .onAdPlaybackState (new AdPlaybackState (adsId ));
393
399
}
394
400
}
395
401
return ;
396
402
}
403
+
397
404
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.
400
408
return ;
401
409
}
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
+
403
420
Window window = timeline .getWindow (0 , new Window ());
404
421
if (window .manifest instanceof HlsManifest ) {
422
+ HlsMediaPlaylist mediaPlaylist = ((HlsManifest ) window .manifest ).mediaPlaylist ;
405
423
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 )));
408
432
}
409
433
putAndNotifyAdPlaybackStateUpdate (adsId , adPlaybackState );
410
434
if (!unsupportedAdsIds .contains (adsId )) {
@@ -464,6 +488,7 @@ public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) {
464
488
adsMediaSource .getAdsId (),
465
489
checkNotNull (adPlaybackState )));
466
490
}
491
+ insertedInterstitialIds .remove (adsId );
467
492
unsupportedAdsIds .remove (adsId );
468
493
}
469
494
@@ -488,6 +513,7 @@ private void putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adP
488
513
eventListener .onAdPlaybackState (adPlaybackState );
489
514
} else {
490
515
activeAdPlaybackStates .remove (adsId );
516
+ insertedInterstitialIds .remove (adsId );
491
517
}
492
518
}
493
519
}
@@ -498,10 +524,6 @@ private void notifyListeners(Consumer<Listener> callable) {
498
524
}
499
525
}
500
526
501
- private static boolean isSupportedMediaItem (MediaItem mediaItem , Timeline timeline ) {
502
- return isHlsMediaItem (mediaItem ) && !isLiveMediaItem (mediaItem , timeline );
503
- }
504
-
505
527
private static boolean isLiveMediaItem (MediaItem mediaItem , Timeline timeline ) {
506
528
int windowIndex = timeline .getFirstWindowIndex (/* shuffleModeEnabled= */ false );
507
529
Window window = new Window ();
@@ -523,68 +545,161 @@ private static boolean isHlsMediaItem(MediaItem mediaItem) {
523
545
|| Util .inferContentType (localConfiguration .uri ) == C .CONTENT_TYPE_HLS ;
524
546
}
525
547
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 );
530
612
if (interstitial .assetUri == null ) {
531
613
Log .w (TAG , "Ignoring interstitials with X-ASSET-LIST. Not yet supported." );
532
614
continue ;
533
615
}
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 ;
539
621
} else {
540
- positionUs = interstitial .startDateUnixUs - hlsMediaPlaylist .startTimeUs ;
622
+ timeUs = interstitial .startDateUnixUs - mediaPlaylist .startTimeUs ;
541
623
}
542
- // Check whether and at which index to insert an ad group for the interstitial start time.
543
624
int adGroupIndex =
544
- adPlaybackState .getAdGroupIndexForPositionUs (
545
- positionUs , /* periodDurationUs= */ hlsMediaPlaylist .durationUs );
625
+ adPlaybackState .getAdGroupIndexForPositionUs (timeUs , mediaPlaylist .durationUs );
546
626
if (adGroupIndex == C .INDEX_UNSET ) {
547
627
// There is no ad group before or at the interstitials position.
548
628
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 ) {
551
631
// There is an ad group before the interstitials. Insert after that index.
552
632
adGroupIndex ++;
553
- adPlaybackState = adPlaybackState .withNewAdGroup (adGroupIndex , positionUs );
633
+ adPlaybackState = adPlaybackState .withNewAdGroup (adGroupIndex , timeUs );
554
634
}
635
+ adPlaybackState =
636
+ insertOrUpdateInterstitialInAdGroup (interstitial , adGroupIndex , adPlaybackState );
637
+ insertedInterstitialIds .add (interstitial .id );
638
+ }
639
+ return adPlaybackState ;
640
+ }
555
641
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 ) {
577
677
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 ());
584
685
}
585
686
return adPlaybackState ;
586
687
}
587
688
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
+
588
703
private static long getInterstitialDurationUs (Interstitial interstitial , long defaultDurationUs ) {
589
704
if (interstitial .playoutLimitUs != C .TIME_UNSET ) {
590
705
return interstitial .playoutLimitUs ;
0 commit comments