From 331d5479e6d59a8f8a1d6f38600c5b8036c889bf Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 29 Mar 2023 14:21:59 +0000 Subject: [PATCH] Do not use content timeline to calculate current position In multi-period live streams, we can't use the content timeline if we want to lookup a period from the public timeline by index or uid because it may be that the content timeline has already been refreshed in the `ImaServerSideAdInsertionMediaSource` but hasn't yet arrived in `ExoPlayerImpl`. This change is taking the current position that needs to be reported to the SDK every 200ms is not relying on the content timeline. PiperOrigin-RevId: 520328126 --- .../ImaServerSideAdInsertionMediaSource.java | 80 ++++++++++++------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index c1e8711b03..1134a97266 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -91,6 +91,7 @@ import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.StreamDisplayContainer; import com.google.ads.interactivemedia.v3.api.StreamManager; import com.google.ads.interactivemedia.v3.api.StreamRequest; +import com.google.ads.interactivemedia.v3.api.StreamRequest.StreamFormat; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.ads.interactivemedia.v3.api.player.VideoStreamPlayer; import com.google.common.collect.ImmutableList; @@ -102,6 +103,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -158,7 +160,10 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou public MediaSource createMediaSource(MediaItem mediaItem) { checkNotNull(mediaItem.localConfiguration); Player player = checkNotNull(adsLoader.player); - StreamPlayer streamPlayer = new StreamPlayer(player, mediaItem); + Uri streamRequestUri = checkNotNull(mediaItem.localConfiguration).uri; + StreamRequest streamRequest = + ImaServerSideAdInsertionUriBuilder.createStreamRequest(streamRequestUri); + StreamPlayer streamPlayer = new StreamPlayer(player, mediaItem, streamRequest); ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); StreamDisplayContainer streamDisplayContainer = createStreamDisplayContainer(imaSdkFactory, adsLoader.configuration, streamPlayer); @@ -167,8 +172,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou adsLoader.context, adsLoader.configuration.imaSdkSettings, streamDisplayContainer); ImaServerSideAdInsertionMediaSource mediaSource = new ImaServerSideAdInsertionMediaSource( - mediaItem, player, + mediaItem, + streamRequest, adsLoader, imaAdsLoader, streamPlayer, @@ -512,16 +518,18 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou private AdPlaybackState adPlaybackState; private ImaServerSideAdInsertionMediaSource( - MediaItem mediaItem, Player player, + MediaItem mediaItem, + StreamRequest streamRequest, AdsLoader adsLoader, com.google.ads.interactivemedia.v3.api.AdsLoader sdkAdsLoader, StreamPlayer streamPlayer, MediaSource.Factory contentMediaSourceFactory, @Nullable AdEventListener applicationAdEventListener, - @Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener) { - this.mediaItem = mediaItem; + @Nullable AdErrorListener applicationAdErrorListener) { this.player = player; + this.mediaItem = mediaItem; + this.streamRequest = streamRequest; this.adsLoader = adsLoader; this.sdkAdsLoader = sdkAdsLoader; this.streamPlayer = streamPlayer; @@ -535,7 +543,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou adsId = ImaServerSideAdInsertionUriBuilder.getAdsId(streamRequestUri); loadVideoTimeoutMs = ImaServerSideAdInsertionUriBuilder.getLoadVideoTimeoutMs(streamRequestUri); streamRequest = ImaServerSideAdInsertionUriBuilder.createStreamRequest(streamRequestUri); - boolean isDashStream = streamRequest.getFormat().equals(StreamRequest.StreamFormat.DASH); + boolean isDashStream = Objects.equals(streamRequest.getFormat(), StreamFormat.DASH); componentListener = new ComponentListener( isLiveStream @@ -679,7 +687,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou private void invalidateServerSideAdInsertionAdPlaybackState() { if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) { ImmutableMap splitAdPlaybackStates; - if (streamRequest.getFormat() == StreamRequest.StreamFormat.DASH) { + if (streamRequest.getFormat() == StreamFormat.DASH) { // DASH ad groups are always split by period. splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline); } else { @@ -1096,6 +1104,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou private final MediaItem mediaItem; private final Timeline.Window window; private final Timeline.Period period; + private final boolean isDashStream; private ImmutableMap adPlaybackStates; @Nullable private Timeline contentTimeline; @@ -1103,9 +1112,10 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @Nullable private StreamLoadListener streamLoadListener; /** Creates an instance. */ - public StreamPlayer(Player player, MediaItem mediaItem) { + public StreamPlayer(Player player, MediaItem mediaItem, StreamRequest streamRequest) { this.player = player; this.mediaItem = mediaItem; + this.isDashStream = streamRequest.getFormat() == StreamFormat.DASH; callbacks = new ArrayList<>(/* initialCapacity= */ 1); adPlaybackStates = ImmutableMap.of(); window = new Timeline.Window(); @@ -1171,30 +1181,42 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou int currentPeriodIndex = player.getCurrentPeriodIndex(); timeline.getPeriod(currentPeriodIndex, period, /* setIds= */ true); timeline.getWindow(player.getCurrentMediaItemIndex(), window); - - // We need the period of the content timeline because its period UIDs are the key used in the - // ad playback state map. The period UIDs of the public timeline are different (masking). - Timeline.Period contentPeriod = + long streamPositionMs; + if (isDashStream && window.isLive()) { + // In multi-period live streams, we can't assume to find the same period in both timelines + // with a given period index. Calculate stream position from the period structure instead. + streamPositionMs = + player.isPlayingAd() + ? window.windowStartTimeMs + + usToMs(period.positionInWindowUs) + + player.getCurrentPosition() + : window.windowStartTimeMs + player.getContentPosition(); + } else { + // The map of ad playback states is keyed with the period UID of the content timeline. In + // timelines that do not change the periods (VOD and single period live), we can use the + // period index in both timelines. + Timeline.Period contentPeriod = + checkNotNull(contentTimeline) + .getPeriod( + currentPeriodIndex - window.firstPeriodIndex, + new Timeline.Period(), + /* setIds= */ true); + AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(contentPeriod.uid)); + // Calculate the stream position from the current position and the playback state. + streamPositionMs = + usToMs(ServerSideAdInsertionUtil.getStreamPositionUs(player, adPlaybackState)); + if (window.windowStartTimeMs != C.TIME_UNSET) { + // Add the time since epoch at start of the window for live streams. + streamPositionMs += window.windowStartTimeMs + period.getPositionInWindowMs(); + } else if (currentPeriodIndex > window.firstPeriodIndex) { + // Add the end position of the previous period in the underlying stream. checkNotNull(contentTimeline) .getPeriod( - currentPeriodIndex - window.firstPeriodIndex, - new Timeline.Period(), + currentPeriodIndex - window.firstPeriodIndex - 1, + contentPeriod, /* setIds= */ true); - AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(contentPeriod.uid)); - - long streamPositionMs = - usToMs(ServerSideAdInsertionUtil.getStreamPositionUs(player, adPlaybackState)); - if (window.windowStartTimeMs != C.TIME_UNSET) { - // Add the time since epoch at start of the window for live streams. - streamPositionMs += window.windowStartTimeMs + period.getPositionInWindowMs(); - } else if (currentPeriodIndex > window.firstPeriodIndex) { - // Add the end position of the previous period in the underlying stream. - checkNotNull(contentTimeline) - .getPeriod( - currentPeriodIndex - window.firstPeriodIndex - 1, - contentPeriod, - /* setIds= */ true); - streamPositionMs += usToMs(contentPeriod.positionInWindowUs + contentPeriod.durationUs); + streamPositionMs += usToMs(contentPeriod.positionInWindowUs + contentPeriod.durationUs); + } } return new VideoProgressUpdate( streamPositionMs,