From dc887070c8cede760a74b2a719aa9ed44b5682f2 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 22 Nov 2021 19:28:40 +0000 Subject: [PATCH] Don't drop updates of the playing period for skipped SSI ads Before this change ExpPlayerImplInternal dropped a change of the playing period when a change in the timeline occurred that actually changed the playing period but we don't want to update the period queue. This logic also dropped the update of a skipped server side inserted preroll ad for which we want the periodQueue to 'seek' to the stream position after the preroll ad and trigger a SKIP discontinuity. This change now introduces an exception so that a skipped SSI ad is still causing an update in the period queue which leads to a 'seek' and a discontinuity of type SKIP. PiperOrigin-RevId: 411607299 --- .../java/androidx/media3/common/Timeline.java | 17 + .../exoplayer/ExoPlayerImplInternal.java | 40 +- .../ImaServerSideDaiMediaSourceFactory.java | 1005 ----------------- 3 files changed, 49 insertions(+), 1013 deletions(-) delete mode 100644 libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideDaiMediaSourceFactory.java diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index 55c7763b9d..b97b4c21d4 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -15,6 +15,7 @@ */ package androidx.media3.common; +import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkState; import static java.lang.Math.max; @@ -819,6 +820,22 @@ public abstract class Timeline implements Bundleable { return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET; } + /** + * Returns the state of the ad at index {@code adIndexInAdGroup} in the ad group at {@code + * adGroupIndex}, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet known. + * + * @param adGroupIndex The ad group index. + * @return The state of the ad, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet + * known. + */ + @UnstableApi + public int getAdState(int adGroupIndex, int adIndexInAdGroup) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); + return adGroup.count != C.LENGTH_UNSET + ? adGroup.states[adIndexInAdGroup] + : AD_STATE_UNAVAILABLE; + } + /** * Returns the position offset in the first unplayed ad at which to begin playback, in * microseconds. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index d36eb49fd1..37a70cd128 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -28,6 +28,7 @@ import android.os.SystemClock; import android.util.Pair; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import androidx.media3.common.AdPlaybackState; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.IllegalSeekPositionException; @@ -2656,15 +2657,14 @@ import java.util.concurrent.atomic.AtomicBoolean; && earliestCuePointIsUnchangedOrLater; // Drop update if the change is from/to server-side inserted ads at the same content position to // avoid any unintentional renderer reset. - timeline.getPeriodByUid(newPeriodUid, period); boolean isInStreamAdChange = - sameOldAndNewPeriodUid - && !isUsingPlaceholderPeriod - && oldContentPositionUs == newContentPositionUs - && ((periodIdWithAds.isAd() - && period.isServerSideInsertedAdGroup(periodIdWithAds.adGroupIndex)) - || (oldPeriodId.isAd() - && period.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex))); + isIgnorableServerSideAdInsertionPeriodChange( + isUsingPlaceholderPeriod, + oldPeriodId, + oldContentPositionUs, + periodIdWithAds, + timeline.getPeriodByUid(newPeriodUid, period), + newContentPositionUs); MediaPeriodId newPeriodId = onlyNextAdGroupIndexIncreased || isInStreamAdChange ? oldPeriodId : periodIdWithAds; @@ -2690,6 +2690,30 @@ import java.util.concurrent.atomic.AtomicBoolean; setTargetLiveOffset); } + private static boolean isIgnorableServerSideAdInsertionPeriodChange( + boolean isUsingPlaceholderPeriod, + MediaPeriodId oldPeriodId, + long oldContentPositionUs, + MediaPeriodId newPeriodId, + Timeline.Period newPeriod, + long newContentPositionUs) { + if (isUsingPlaceholderPeriod + || oldContentPositionUs != newContentPositionUs + || !oldPeriodId.periodUid.equals(newPeriodId.periodUid)) { + // The period position changed. + return false; + } + if (oldPeriodId.isAd() && newPeriod.isServerSideInsertedAdGroup(oldPeriodId.adGroupIndex)) { + // Whether the old period was a server side ad that doesn't need skipping to the content. + return newPeriod.getAdState(oldPeriodId.adGroupIndex, oldPeriodId.adIndexInAdGroup) + != AdPlaybackState.AD_STATE_ERROR + && newPeriod.getAdState(oldPeriodId.adGroupIndex, oldPeriodId.adIndexInAdGroup) + != AdPlaybackState.AD_STATE_SKIPPED; + } + // If the new period is a server side inserted ad, we can just continue playing. + return newPeriodId.isAd() && newPeriod.isServerSideInsertedAdGroup(newPeriodId.adGroupIndex); + } + private static boolean isUsingPlaceholderPeriod( PlaybackInfo playbackInfo, Timeline.Period period) { MediaPeriodId periodId = playbackInfo.periodId; diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideDaiMediaSourceFactory.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideDaiMediaSourceFactory.java deleted file mode 100644 index 47899bcf22..0000000000 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideDaiMediaSourceFactory.java +++ /dev/null @@ -1,1005 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.exoplayer.ima; - -import static androidx.media3.common.util.Assertions.checkNotNull; -import static java.nio.charset.StandardCharsets.UTF_8; - -import android.content.Context; -import android.net.Uri; -import android.view.ViewGroup; -import androidx.annotation.CallSuper; -import androidx.annotation.Nullable; -import androidx.media3.common.AdOverlayInfo; -import androidx.media3.common.AdPlaybackState; -import androidx.media3.common.AdViewProvider; -import androidx.media3.common.C; -import androidx.media3.common.MediaItem; -import androidx.media3.common.Metadata; -import androidx.media3.common.Player; -import androidx.media3.common.Timeline; -import androidx.media3.common.util.Assertions; -import androidx.media3.common.util.ConditionVariable; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.common.util.Util; -import androidx.media3.datasource.HttpDataSource; -import androidx.media3.datasource.TransferListener; -import androidx.media3.exoplayer.drm.DrmSessionManager; -import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; -import androidx.media3.exoplayer.source.CompositeMediaSource; -import androidx.media3.exoplayer.source.MediaPeriod; -import androidx.media3.exoplayer.source.MediaSource; -import androidx.media3.exoplayer.source.MediaSourceFactory; -import androidx.media3.exoplayer.source.ads.ServerSideInsertedAdsMediaSource; -import androidx.media3.exoplayer.source.ads.ServerSideInsertedAdsUtil; -import androidx.media3.exoplayer.upstream.Allocator; -import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; -import androidx.media3.exoplayer.upstream.Loader; -import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction; -import androidx.media3.exoplayer.upstream.Loader.Loadable; -import androidx.media3.extractor.metadata.emsg.EventMessage; -import androidx.media3.extractor.metadata.id3.TextInformationFrame; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent; -import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdEvent; -import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; -import com.google.ads.interactivemedia.v3.api.CuePoint; -import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; -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.player.VideoAdPlayer; -import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; -import com.google.ads.interactivemedia.v3.api.player.VideoStreamPlayer; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; - -/** Creates instances of {@link MediaSource} that are specific to IMA DAI ads playback. */ -@UnstableApi -public final class ImaServerSideDaiMediaSourceFactory implements MediaSourceFactory { - - /** Builder for {@link ImaServerSideDaiMediaSourceFactory}. */ - public static final class Builder { - - private final MediaSourceFactory childStreamsMediaSourceFactory; - private final Context context; - private final PlayerProvider playerProvider; - private final ViewGroup adsContainer; - private final AdErrorListener adErrorListener; - - @Nullable private ImaSdkSettings imaSdkSettings; - @Nullable private AdEventListener adEventListener; - @Nullable private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback; - @Nullable private List companionAdSlots; - @Nullable private AdViewProvider adViewProvider; - - private boolean debugModeEnabled; - - /** Creates a new builder for {@link ImaServerSideDaiMediaSourceFactory}. */ - public Builder( - MediaSourceFactory childStreamsMediaSourceFactory, - Context context, - PlayerProvider playerProvider, - ViewGroup adsContainer, - AdErrorListener adErrorListener) { - this.childStreamsMediaSourceFactory = checkNotNull(childStreamsMediaSourceFactory); - this.context = checkNotNull(context).getApplicationContext(); - this.playerProvider = checkNotNull(playerProvider); - this.adsContainer = checkNotNull(adsContainer); - this.adErrorListener = checkNotNull(adErrorListener); - } - - /** - * Sets the IMA SDK settings. The provided settings instance's player type and version fields - * may be overwritten. - * - *

If this method is not called the default settings will be used. - * - * @param imaSdkSettings The {@link ImaSdkSettings}. - * @return This builder, for convenience. - */ - public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { - this.imaSdkSettings = checkNotNull(imaSdkSettings); - return this; - } - - /** - * Sets a listener for ad events that will be passed to {@link - * AdsManager#addAdEventListener(AdEventListener)}. - * - * @param adEventListener The ad event listener. - * @return This builder, for convenience. - */ - public Builder setAdEventListener(AdEventListener adEventListener) { - this.adEventListener = checkNotNull(adEventListener); - return this; - } - - /** - * Sets a callback to receive video ad player events. Note that these events are handled - * internally by the IMA SDK and the medias source being build by this builder. For analytics - * and diagnostics, new implementations should generally use events from the top-level {@link - * Player.Listener top-level listeners} instead of setting a callback via this method. - * - * @param videoAdPlayerCallback The callback to receive video ad player events. - * @return This builder, for convenience. - * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback - */ - public Builder setVideoAdPlayerCallback( - VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) { - this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback); - return this; - } - - /** - * Sets the slots to use for companion ads, if they are present in the loaded ad. - * - * @param companionAdSlots The slots to use for companion ads. - * @return This builder, for convenience. - * @see AdDisplayContainer#setCompanionSlots(Collection) - */ - public Builder setCompanionAdSlots(Collection companionAdSlots) { - this.companionAdSlots = ImmutableList.copyOf(checkNotNull(companionAdSlots)); - return this; - } - - /** - * Sets the {@link AdViewProvider} that provides information about views for the ad playback UI. - * - * @param adViewProvider A provider for {@link ViewGroup} instances. - * @return This builder, for convenience. - */ - public Builder setAdViewProvider(@Nullable AdViewProvider adViewProvider) { - this.adViewProvider = adViewProvider; - return this; - } - - /** - * Sets whether to enable outputting verbose logs for the IMA extension and IMA SDK. The default - * value is {@code false}. This setting is intended for debugging only, and should not be - * enabled in production applications. - * - * @param debugModeEnabled Whether to enable outputting verbose logs for the IMA extension and - * IMA SDK. - * @return This builder, for convenience. - * @see ImaSdkSettings#setDebugMode(boolean) - */ - public Builder setDebugModeEnabled(boolean debugModeEnabled) { - this.debugModeEnabled = debugModeEnabled; - return this; - } - - /** Returns a new {@link ImaServerSideDaiMediaSourceFactory}. */ - public ImaServerSideDaiMediaSourceFactory build() { - DaiStreamPlayer streamPlayer = new DaiStreamPlayer(playerProvider); - return new ImaServerSideDaiMediaSourceFactory( - childStreamsMediaSourceFactory, - context, - playerProvider, - streamPlayer, - adsContainer, - adViewProvider, - new ImaUtil.DaiConfiguration( - adErrorListener, - companionAdSlots, - adEventListener, - videoAdPlayerCallback, - imaSdkSettings, - debugModeEnabled)); - } - } - - /** Provides {@link Player} instances. */ - public interface PlayerProvider { - - /** - * Returns an {@link Player} instance. - * - *

This method is called each time a {@link MediaSource} is created from a {@link MediaItem} - * that represents DAI stream. - */ - Player getPlayer(); - } - - /** Simplified and more targeted ad state representation within stream for DAI ads. */ - private interface AdState { - - AdPlaybackState getAdPlaybackState(); - - /** - * Updates the {@link AdPlaybackState} with new ad information. - * - * @param postroll Ad belongs to a postroll ad break. - * @param adStartUs The ad start position, in microseconds. - * @param adDurationUs The ad duration, in microseconds. - */ - void handleAdLoaded(boolean postroll, long adStartUs, long adDurationUs); - - /** - * Sets the ad breaks/cue points. - * - * @param adGroupTimesUs A list of cuepoints. - */ - void addAdBreaks(long[] adGroupTimesUs); - - /** - * Called when an ad is skipped. Puts that ad in a skipped state. - * - * @param adPosition The position of the ad within the pod. - */ - void handleAdSkipped(int adPosition); - - /** Called when an ad break ends. */ - void handleAdBreakEnded(); - } - - /** - * A listener for stream load. IMA sdk will send stream data when stream finishes initialization. - */ - private interface DaiStreamLoadListener { - - /** - * Loads a stream with dynamic ad insertion given the stream url and subtitles array. The - * subtitles array is only used in VOD streams. - * - *

Each entry in the subtitles array is a HashMap that corresponds to a language. Each map - * will have a "language" key with a two letter language string value, a "language name" to - * specify the set of subtitles if multiple sets exist for the same language, and one or more - * subtitle key/value pairs. Here's an example the map for English: - * - *

"language" -> "en" "language_name" -> "English" "webvtt" -> - * "https://example.com/vtt/en.vtt" "ttml" -> "https://example.com/ttml/en.ttml" - */ - void onLoadStream(String streamUri, List> subtitles); - } - - // Entities shared by all IMA DAI media sources. - private final MediaSourceFactory childStreamsMediaSourceFactory; - private final ImaSdkFactory imaSdkFactory; - private final ImaSdkSettings imaSdkSettings; - private final PlayerProvider playerProvider; - private final DaiStreamPlayer streamPlayerForSdk; - private final ImaUtil.DaiConfiguration config; - private final Context context; - private final StreamDisplayContainer container; - - private ImaServerSideDaiMediaSourceFactory( - MediaSourceFactory childStreamsMediaSourceFactory, - Context context, - PlayerProvider playerProvider, - DaiStreamPlayer streamPlayerForSdk, - ViewGroup adsContainer, - @Nullable AdViewProvider adViewProvider, - ImaUtil.DaiConfiguration config) { - imaSdkFactory = ImaSdkFactory.getInstance(); - this.childStreamsMediaSourceFactory = childStreamsMediaSourceFactory; - this.context = context; - this.playerProvider = playerProvider; - this.streamPlayerForSdk = streamPlayerForSdk; - this.config = config; - container = ImaSdkFactory.createStreamDisplayContainer(adsContainer, streamPlayerForSdk); - if (config.companionAdSlots != null) { - container.setCompanionSlots(config.companionAdSlots); - } - imaSdkSettings = - config.imaSdkSettings == null - ? imaSdkFactory.createImaSdkSettings() - : config.imaSdkSettings; - imaSdkSettings.setLanguage(Util.getSystemLanguageCodes()[0]); - if (config.debugModeEnabled) { - imaSdkSettings.setDebugMode(true); - } - registerFriendlyObstructions(container, adViewProvider); - } - - @Override - public MediaSourceFactory setDrmSessionManagerProvider( - @Nullable DrmSessionManagerProvider drmSessionManagerProvider) { - childStreamsMediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider); - return this; - } - - @Deprecated - @Override - public MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { - return this; - } - - @Deprecated - @Override - public MediaSourceFactory setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - return this; - } - - @Deprecated - @Override - public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) { - return this; - } - - @Override - public MediaSourceFactory setLoadErrorHandlingPolicy( - @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { - childStreamsMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy); - return this; - } - - @Override - public int[] getSupportedTypes() { - return new int[] {C.TYPE_DASH, C.TYPE_HLS}; - } - - @Override - public MediaSource createMediaSource(MediaItem mediaItem) { - // Ads loader can be shared, but it is not recommended. Each media source will use its own ads - // loader to handle stream request. - AdsLoader adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings, container); - DaiMediaSource daiMediaSource = - new DaiMediaSource( - mediaItem, - playerProvider.getPlayer(), - childStreamsMediaSourceFactory, - adsLoader, - config, - streamPlayerForSdk); - streamPlayerForSdk.mediaSourceCreated(daiMediaSource); - return daiMediaSource; - } - - private void registerFriendlyObstructions( - StreamDisplayContainer container, @Nullable AdViewProvider adViewProvider) { - if (adViewProvider != null) { - for (AdOverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) { - checkNotNull(overlayInfo.reasonDetail); - container.registerFriendlyObstruction( - imaSdkFactory.createFriendlyObstruction( - overlayInfo.view, - ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose), - overlayInfo.reasonDetail)); - } - } - } - - /** Loads all the required data for a stream with ads. */ - private static class StreamManagerLoadable - implements Loadable, AdsLoader.AdsLoadedListener, AdErrorEvent.AdErrorListener { - - private final ImaUtil.DaiConfiguration config; - private final AdsLoader adsLoader; - private final DaiStreamPlayer streamPlayerForSdk; - private final StreamRequest request; - @Nullable private StreamManager streamManager; - @Nullable private Uri streamManifestUri; - - public StreamManagerLoadable( - ImaUtil.DaiConfiguration config, - AdsLoader adsLoader, - StreamRequest request, - DaiStreamPlayer streamPlayerForSdk) { - this.config = checkNotNull(config); - this.adsLoader = checkNotNull(adsLoader); - this.request = checkNotNull(request); - this.streamPlayerForSdk = checkNotNull(streamPlayerForSdk); - } - - @Override - public void cancelLoad() { - // No-op, we never cancel load. - } - - @Override - public void load() { - final ConditionVariable conditionVariable = new ConditionVariable(); - // SDK will call loadUrl on stream player for SDK once manifest uri is available. - streamPlayerForSdk.setStreamLoadListener( - (streamUri, subtitles) -> { - streamManifestUri = Uri.parse(streamUri); - conditionVariable.open(); - }); - adsLoader.addAdsLoadedListener(this); - adsLoader.addAdErrorListener(this); - // We need to inform integrating app about errors within the ads loader - if (config.applicationAdErrorListener != null) { - adsLoader.addAdErrorListener(config.applicationAdErrorListener); - } - adsLoader.requestStream(request); - conditionVariable.blockUninterruptible(); - } - - public Uri getStreamUri() { - checkNotNull(streamManifestUri); - return streamManifestUri; - } - - @Nullable - public StreamManager getStreamManager() { - return streamManager; - } - - // AdsLoader.AdsLoadedListener implementation. - - @Override - public void onAdsManagerLoaded(AdsManagerLoadedEvent event) { - removeAdsLoaderListeners(); - streamManager = event.getStreamManager(); - // We need to inform integrating app about ad events within the stream manager. - if (config.applicationAdEventListener != null) { - streamManager.addAdEventListener(config.applicationAdEventListener); - } - // We need to inform integrating app about errors within the stream manager. - if (config.applicationAdErrorListener != null) { - streamManager.addAdErrorListener(config.applicationAdErrorListener); - } - // Init triggers stream initialization which leads to stream manifest uri provided in a - // callback. - streamManager.init(); - } - - // AdErrorEvent.AdErrorListener implementation. - - @Override - public void onAdError(AdErrorEvent adErrorEvent) { - removeAdsLoaderListeners(); - } - - /** Cleans up stream manager. */ - public void release() { - removeAdsLoaderListeners(); - if (streamManager != null) { - if (config.applicationAdEventListener != null) { - streamManager.removeAdEventListener(config.applicationAdEventListener); - } - // We need to inform integrating app about errors within the stream manager. - if (config.applicationAdErrorListener != null) { - streamManager.removeAdErrorListener(config.applicationAdErrorListener); - } - streamManager.destroy(); - streamManager = null; - } - } - - /** Remove all listeners after ads loader succeeded or errored out. */ - private void removeAdsLoaderListeners() { - adsLoader.removeAdsLoadedListener(this); - adsLoader.removeAdErrorListener(this); - if (config.applicationAdErrorListener != null) { - adsLoader.removeAdErrorListener(config.applicationAdErrorListener); - } - } - } - - /** - * Listens to the main exoplayer instance and communicates with IMA sdk to react to sdk callbacks - * as well as update sdk about exoplayer state. - */ - private static final class DaiStreamPlayer implements VideoStreamPlayer, Player.Listener { - - private final PlayerProvider playerProvider; - private final List callbacks; - - @Nullable private ImaServerSideDaiMediaSourceFactory.DaiStreamLoadListener streamLoadListener; - @Nullable private AdState adState; - @Nullable private Player player; - - public DaiStreamPlayer(PlayerProvider playerProvider) { - this.playerProvider = playerProvider; - this.callbacks = new ArrayList<>(/* initialCapacity= */ 1); - } - - public void mediaSourceCreated(AdState adState) { - player = playerProvider.getPlayer(); - // Multiple add calls result in just one listener added when listener is the same object. - player.addListener(this); - this.adState = adState; - } - - public void setStreamLoadListener( - ImaServerSideDaiMediaSourceFactory.DaiStreamLoadListener listener) { - streamLoadListener = Assertions.checkNotNull(listener); - } - - public void release() { - callbacks.clear(); - streamLoadListener = null; - if (player != null) { - player.removeListener(this); - } - } - - private void triggerContentComplete() { - for (VideoStreamPlayer.VideoStreamPlayerCallback callback : callbacks) { - callback.onContentComplete(); - } - } - - private void triggerUserTextReceived(String userText) { - for (VideoStreamPlayer.VideoStreamPlayerCallback callback : callbacks) { - callback.onUserTextReceived(userText); - } - } - - private void triggerVolumeChanged(int percentage) { - for (VideoStreamPlayer.VideoStreamPlayerCallback callback : callbacks) { - callback.onVolumeChanged(percentage); - } - } - - // VideoStreamPlayer interface methods called by the sdk. Some of these methods are no-op, - // because they do not make sense in the DAI plugin context. - - @Override - public void loadUrl(String url, List> subtitles) { - if (streamLoadListener != null) { - // SDK provided manifest url, notify the listener. - streamLoadListener.onLoadStream(url, subtitles); - } - } - - @Override - public void addCallback(VideoStreamPlayer.VideoStreamPlayerCallback callback) { - callbacks.add(callback); - } - - @Override - public void removeCallback(VideoStreamPlayer.VideoStreamPlayerCallback callback) { - callbacks.remove(callback); - } - - @Override - public void onAdBreakStarted() { - // Do nothing. - } - - @Override - public void onAdBreakEnded() { - // Do nothing. - } - - @Override - public void onAdPeriodStarted() { - // Do nothing. - } - - @Override - public void onAdPeriodEnded() { - // Do nothing. - } - - @Override - public void pause() { - // Do nothing. - } - - @Override - public void resume() { - // Do nothing. - } - - @Override - public void seek(long timeMs) { - // TODO(gdambrauskas): skippable ad did nothing when clicking skip button, continued play - // as usual eventhough seek was called with 30s. - if (player != null) { - player.seekTo(timeMs); - } - } - - // From VolumeProvider - @Override - public int getVolume() { - if (player != null) { - return (int) Math.floor(player.getVolume() * 100); - } - return 0; - } - - // From ContentProgressProvider - @Override - public VideoProgressUpdate getContentProgress() { - if (adState == null || adState.getAdPlaybackState() == null) { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - checkNotNull(adState); - checkNotNull(player); - long positionMs = - Util.usToMs( - ServerSideInsertedAdsUtil.getStreamPositionUs(player, adState.getAdPlaybackState())); - checkNotNull(adState); - checkNotNull(player); - long durationMs = - Util.usToMs( - ServerSideInsertedAdsUtil.getStreamDurationUs(player, adState.getAdPlaybackState())); - return new VideoProgressUpdate(positionMs, durationMs); - } - - // Listen and handle Exoplayer events we care about. - // From Player.Listener interface. - @Override - public void onMetadata(Metadata metadata) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof TextInformationFrame) { - TextInformationFrame textFrame = (TextInformationFrame) entry; - if ("TXXX".equals(textFrame.id)) { - triggerUserTextReceived(textFrame.value); - } - } else if (entry instanceof EventMessage) { - EventMessage eventMessage = (EventMessage) entry; - String eventMessageValue = new String(eventMessage.messageData, UTF_8); - triggerUserTextReceived(eventMessageValue); - } - } - } - - // From Player.EventListener - @Override - public void onPlaybackStateChanged(int playbackState) { - switch (playbackState) { - case Player.STATE_ENDED: - triggerContentComplete(); - break; - default: - break; - } - } - - // From Player.Listener - @Override - public void onVolumeChanged(float volume) { - int volumePct = (int) Math.floor(volume * 100); - triggerVolumeChanged(volumePct); - } - - /** - * Returns the playback position in the current content window or ad, in milliseconds, or the - * prospective position in milliseconds if the {@link Player#getCurrentTimeline() current - * timeline} is empty. - */ - public long getCurrentPosition() { - checkNotNull(player); - return player.getCurrentPosition(); - } - } - - /** Media source for IMA streams with inserted ads. */ - private static final class DaiMediaSource extends CompositeMediaSource - implements Player.Listener, - ImaServerSideDaiMediaSourceFactory.AdState, - AdEvent.AdEventListener { - - private final MediaItem mediaItem; - private final Player player; - // Factory used to construct child media source, which is the concrete media source playing the - // stream. - private final MediaSourceFactory mediaSourceFactory; - private final StreamManagerReadyCallback streamManagerReadyCallback; - private final StreamManagerLoadable streamManagerLoadable; - - private int adBreakIndex = 0; - private AdPlaybackState adPlaybackState; - private Object childSourceWindowUid; - - // VOD has a fixed number of ad breaks. Allows to create more ad groups (for live streams) vs - // adding more ads to the existing ad groups (for VOD). - @Nullable private long[] knownAdBreaksCuepoints = null; - @Nullable private ServerSideInsertedAdsMediaSource mediaSource; - @Nullable private Loader loader; - @Nullable private IOException loadError; - - public DaiMediaSource( - MediaItem mediaItem, - Player player, - MediaSourceFactory mediaSourceFactory, - AdsLoader adsLoader, - ImaUtil.DaiConfiguration config, - DaiStreamPlayer streamPlayerForSdk) { - checkNotNull(mediaItem.localConfiguration); - this.mediaItem = mediaItem; - this.player = player; - this.mediaSourceFactory = mediaSourceFactory; - adPlaybackState = - new AdPlaybackState( - /* adsId= */ new Object(), /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE) - .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true); - childSourceWindowUid = new Object(); - - // TODO(gdambrauskas): pass in loadable from outside, simplifies testing. - checkNotNull(mediaItem.localConfiguration); - StreamRequest request = - DaiStreamRequest.fromUri(mediaItem.localConfiguration.uri).getStreamRequest(); - streamManagerLoadable = - new StreamManagerLoadable(config, adsLoader, request, streamPlayerForSdk); - - streamManagerReadyCallback = new DaiMediaSource.StreamManagerReadyCallback(); - player.addListener(this); - } - - @Override - protected void releaseSourceInternal() { - super.releaseSourceInternal(); - player.removeListener(this); - StreamManager manager = streamManagerLoadable.getStreamManager(); - checkNotNull(manager); - if (manager != null) { - manager.removeAdEventListener(this); - } - streamManagerLoadable.release(); - } - - @Override - public MediaItem getMediaItem() { - return mediaItem; - } - - @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); - loader = new Loader("DaiMediaSource"); - loader.startLoading( - streamManagerLoadable, streamManagerReadyCallback, /* defaultMinRetryCount= */ 0); - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { - checkNotNull(mediaSource); - return mediaSource.createPeriod(id, allocator, startPositionUs); - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - checkNotNull(mediaSource); - mediaSource.releasePeriod(mediaPeriod); - } - - @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline newTimeline) { - childSourceWindowUid = newTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).uid; - refreshSourceInfo(newTimeline); - } - - @Override - @CallSuper - public void maybeThrowSourceInfoRefreshError() throws IOException { - super.maybeThrowSourceInfoRefreshError(); - if (loadError != null) { - throw loadError; - } - } - - // ImaServerSideDaiMediaSourceFactory.AdState implementation. - - @Override - public AdPlaybackState getAdPlaybackState() { - return adPlaybackState; - } - - @Override - public void addAdBreaks(long[] adGroupTimesUs) { - adPlaybackState = new AdPlaybackState(/* adsId= */ new Object(), adGroupTimesUs); - // Mark all ad breaks as server side inserted. - for (int i = 0; i < adGroupTimesUs.length; i++) { - adPlaybackState = - adPlaybackState.withIsServerSideInserted( - /* adGroupIndex= */ i, /* isServerSideInserted= */ true); - } - checkNotNull(mediaSource); - mediaSource.setAdPlaybackState(adPlaybackState); - } - - @Override - public void handleAdBreakEnded() { - adBreakIndex++; - } - - @Override - public void handleAdSkipped(int adPosition) { - adPlaybackState = adPlaybackState.withSkippedAd(adBreakIndex, adPosition); - checkNotNull(mediaSource); - mediaSource.setAdPlaybackState(adPlaybackState); - // TODO(gdambrauskas): seek is disabled in exo code when ads are playing, seek does nothing - // here when we try to seek past ad. - } - - @Override - public void handleAdLoaded(boolean postroll, long adStartUs, long adDurationUs) { - long adEndUs = adStartUs + adDurationUs; - if (knownAdBreaksCuepoints != null) { - int adGroupIndex = getAdGroupIndexForKnownCuepoint(adStartUs); - adPlaybackState = - ServerSideInsertedAdsUtil.addAdToAdGroup( - adPlaybackState, adGroupIndex, adStartUs, adEndUs, adDurationUs); - } else { - // When number of ad breaks can grow infinitely (live streams), we treat each ad as its own - // ad break and just keep adding each ad as a new ad break. - adPlaybackState = - ServerSideInsertedAdsUtil.addAdGroupToAdPlaybackState( - adPlaybackState, adStartUs, adEndUs, adDurationUs); - } - // if (postroll) { - // TODO(gdambrauskas): needs testing, not clear what values are expected at the end of - // the stream for postroll for ad break end. Same as midroll? - // adPlaybackState = - // ServerSideInsertedAdsUtil.addAdGroupToAdPlaybackState( - // adPlaybackState, C.TIME_END_OF_SOURCE, adBreakEndUs, adDurationUs); - // } - checkNotNull(mediaSource); - mediaSource.setAdPlaybackState(adPlaybackState); - } - - /** - * Gets ad group index based on ad start time. - * - * @param adStartUs Ad start time. IMA SDK returns same ad start time for every ad within a - * single ad break. - * @return The ad group index. - */ - private int getAdGroupIndexForKnownCuepoint(long adStartUs) { - int adGroupIndex = 0; - checkNotNull(knownAdBreaksCuepoints); - // TODO(gdambrauskas): need to test stream with postroll. - for (long cuepointUs : knownAdBreaksCuepoints) { - if (cuepointUs == adStartUs) { - return adGroupIndex; - } - adGroupIndex++; - } - return -1; - } - - // Player.Listener implementation. - - @Override - public void onPositionDiscontinuity( - Player.PositionInfo oldPosition, - Player.PositionInfo newPosition, - @Player.DiscontinuityReason int reason) { - // Make sure discontinuity is for our child media source. - if (!childSourceWindowUid.equals(oldPosition.windowUid) - || !childSourceWindowUid.equals(newPosition.windowUid)) { - return; - } - if (oldPosition.adGroupIndex != C.INDEX_UNSET && newPosition.adGroupIndex == C.INDEX_UNSET) { - for (int i = 0; i <= oldPosition.adIndexInAdGroup; i++) { - if (adPlaybackState.getAdGroup(oldPosition.adGroupIndex).states[i] - == AdPlaybackState.AD_STATE_SKIPPED) { - // Ads that were skipped, stay in skipped state. - continue; - } - // Mark ads in old ad groups as played. - adPlaybackState = - adPlaybackState.withPlayedAd(oldPosition.adGroupIndex, /* adIndexInAdGroup= */ i); - } - } - checkNotNull(mediaSource); - mediaSource.setAdPlaybackState(adPlaybackState); - } - - /** AdEvent.AdEventListener implementation. */ - @Override - public void onAdEvent(AdEvent event) { - switch (event.getType()) { - case SKIPPED: - // IMA sdk always returns index starting at 1. - handleAdSkipped(event.getAd().getAdPodInfo().getAdPosition() - 1); - break; - case AD_BREAK_ENDED: - handleAdBreakEnded(); - break; - // Cuepoints changed event is available only for VOD streams. - case CUEPOINTS_CHANGED: - // CUEPOINTS_CHANGED is firing multiple times. For a stream with 2 - // ad breaks, there are 2 cue point change events, before preroll and before the - // midroll. Store cuepoints only once. - if (knownAdBreaksCuepoints == null) { - StreamManager manager = streamManagerLoadable.getStreamManager(); - checkNotNull(manager); - knownAdBreaksCuepoints = getAdGroupTimesUsForCuePoints(manager.getCuePoints()); - addAdBreaks(knownAdBreaksCuepoints); - } - break; - case LOADED: - AdPodInfo adPodInfo = event.getAd().getAdPodInfo(); - - // This is an ad belonging to a postroll ad break or DAI live stream (live stream does not - // know entirety of cue points ahead of time). - boolean postroll = adPodInfo.getPodIndex() == -1; - long adStartUs = (long) (adPodInfo.getTimeOffset() * C.MICROS_PER_SECOND); - handleAdLoaded( - postroll, adStartUs, (long) (event.getAd().getDuration() * C.MICROS_PER_SECOND)); - break; - default: - break; - } - } - - /** Invoked when stream manager is initialized and has manifest uri. */ - private final class StreamManagerReadyCallback - implements Loader.Callback { - - @Override - public void onLoadCompleted( - StreamManagerLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - // We only care to listen to ad events. Errors are already reported to the integrating app - // and we can't do anything about an error. - StreamManager manager = loadable.getStreamManager(); - checkNotNull(manager); - manager.addAdEventListener(DaiMediaSource.this); - Uri streamUri = loadable.getStreamUri(); - checkNotNull(streamUri); - MediaSource contentMediaSource = - mediaSourceFactory.createMediaSource(MediaItem.fromUri(streamUri)); - mediaSource = new ServerSideInsertedAdsMediaSource(contentMediaSource); - mediaSource.setAdPlaybackState(adPlaybackState); - prepareChildSource(/* id= */ null, mediaSource); - } - - @Override - public void onLoadCanceled( - StreamManagerLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - boolean released) { - // Load can only be cancelled by us, so this can't really happen. - throw new IllegalStateException("Do not cancel loading of IMA stream manager."); - } - - @Override - public LoadErrorAction onLoadError( - StreamManagerLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - IOException error, - int errorCount) { - loadError = error; - return Loader.DONT_RETRY; - } - } - } - - /** @return List of all the cuepoints. */ - @SuppressWarnings("deprecation") - private static long[] getAdGroupTimesUsForCuePoints(List cuePoints) { - if (cuePoints.isEmpty()) { - return new long[] {0L}; - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (CuePoint cuePoint : cuePoints) { - if (cuePoint.getStartTime() == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = - Util.msToUs((long) Math.floor(cuePoint.getStartTime() * 1000d)); - } - } - return adGroupTimesUs; - } -}