Commit e350f9ce by ibaker Committed by Ian Baker

Publish the ImaServerSideAdInsertionMediaSource

Issue: google/ExoPlayer#8213

#minor-release

PiperOrigin-RevId: 425381474
parent da947c0c
......@@ -83,6 +83,9 @@
([#9615](https://github.com/google/ExoPlayer/issues/9615)).
* Enforce playback speed of 1.0 during ad playback
([#9018](https://github.com/google/ExoPlayer/issues/9018)).
* Add support for
[IMA Dynamic Ad Insertion (DAI)](https://support.google.com/admanager/answer/6147120)
([#8213](https://github.com/google/ExoPlayer/issues/8213)).
* DASH:
* Support the `forced-subtitle` track role
([#9727](https://github.com/google/ExoPlayer/issues/9727)).
......
/*
* 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 com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdPlaybackStateForPeriods;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationAndPropagate;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationInAdGroup;
import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.secToUs;
import static com.google.android.exoplayer2.util.Util.sum;
import static com.google.android.exoplayer2.util.Util.usToMs;
import static java.lang.Math.min;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.view.ViewGroup;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import com.google.ads.interactivemedia.v3.api.Ad;
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.AdsLoadedListener;
import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
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.VideoProgressUpdate;
import com.google.ads.interactivemedia.v3.api.player.VideoStreamPlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.ForwardingTimeline;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionMediaSource;
import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionMediaSource.AdPlaybackStateUpdater;
import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil;
import com.google.android.exoplayer2.ui.AdOverlayInfo;
import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* MediaSource for IMA server side inserted ad streams.
*
* <p>TODO(bachinger) add code snippet from PlayerActivity
*/
public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSource<Void> {
/**
* Factory for creating {@link ImaServerSideAdInsertionMediaSource
* ImaServerSideAdInsertionMediaSources}.
*
* <p>Apps can use the {@link ImaServerSideAdInsertionMediaSource.Factory} to customized the
* {@link DefaultMediaSourceFactory} that is used to build a player:
*
* <p>TODO(bachinger) add code snippet from PlayerActivity
*/
public static final class Factory implements MediaSource.Factory {
private final AdsLoader adsLoader;
private final MediaSource.Factory contentMediaSourceFactory;
/**
* Creates a new factory for {@link ImaServerSideAdInsertionMediaSource
* ImaServerSideAdInsertionMediaSources}.
*
* @param adsLoader The {@link AdsLoader}.
* @param contentMediaSourceFactory The content media source factory to create content sources.
*/
public Factory(AdsLoader adsLoader, MediaSource.Factory contentMediaSourceFactory) {
this.adsLoader = adsLoader;
this.contentMediaSourceFactory = contentMediaSourceFactory;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
contentMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
return this;
}
@Override
public MediaSource.Factory setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
contentMediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
return this;
}
@Override
public int[] getSupportedTypes() {
return contentMediaSourceFactory.getSupportedTypes();
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
Player player = checkNotNull(adsLoader.player);
StreamPlayer streamPlayer = new StreamPlayer(player, mediaItem);
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
StreamDisplayContainer streamDisplayContainer =
createStreamDisplayContainer(imaSdkFactory, adsLoader.configuration, streamPlayer);
com.google.ads.interactivemedia.v3.api.AdsLoader imaAdsLoader =
imaSdkFactory.createAdsLoader(
adsLoader.context, adsLoader.configuration.imaSdkSettings, streamDisplayContainer);
ImaServerSideAdInsertionMediaSource mediaSource =
new ImaServerSideAdInsertionMediaSource(
mediaItem,
player,
imaAdsLoader,
streamPlayer,
contentMediaSourceFactory,
adsLoader.configuration.applicationAdEventListener,
adsLoader.configuration.applicationAdErrorListener);
adsLoader.addMediaSourceResources(mediaSource, streamPlayer, imaAdsLoader);
return mediaSource;
}
}
/** An ads loader for IMA server side ad insertion streams. */
public static final class AdsLoader {
/** Builder for building an {@link AdsLoader}. */
public static final class Builder {
private final Context context;
private final AdViewProvider adViewProvider;
@Nullable private ImaSdkSettings imaSdkSettings;
@Nullable private AdEventListener adEventListener;
@Nullable private AdErrorEvent.AdErrorListener adErrorListener;
private ImmutableList<CompanionAdSlot> companionAdSlots;
/**
* Creates an instance.
*
* @param context A context.
* @param adViewProvider A provider for {@link ViewGroup} instances.
*/
public Builder(Context context, AdViewProvider adViewProvider) {
this.context = context;
this.adViewProvider = adViewProvider;
companionAdSlots = ImmutableList.of();
}
/**
* Sets the IMA SDK settings.
*
* <p>If this method is not called the default settings will be used.
*
* @param imaSdkSettings The {@link ImaSdkSettings}.
* @return This builder, for convenience.
*/
public AdsLoader.Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
this.imaSdkSettings = imaSdkSettings;
return this;
}
/**
* Sets the optional {@link AdEventListener} that will be passed to {@link
* AdsManager#addAdEventListener(AdEventListener)}.
*
* @param adEventListener The ad event listener.
* @return This builder, for convenience.
*/
public AdsLoader.Builder setAdEventListener(AdEventListener adEventListener) {
this.adEventListener = adEventListener;
return this;
}
/**
* Sets the optional {@link AdErrorEvent.AdErrorListener} that will be passed to {@link
* AdsManager#addAdErrorListener(AdErrorEvent.AdErrorListener)}.
*
* @param adErrorListener The {@link AdErrorEvent.AdErrorListener}.
* @return This builder, for convenience.
*/
public AdsLoader.Builder setAdErrorListener(AdErrorEvent.AdErrorListener adErrorListener) {
this.adErrorListener = adErrorListener;
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 AdsLoader.Builder setCompanionAdSlots(Collection<CompanionAdSlot> companionAdSlots) {
this.companionAdSlots = ImmutableList.copyOf(companionAdSlots);
return this;
}
/** Returns a new {@link AdsLoader}. */
public AdsLoader build() {
@Nullable ImaSdkSettings imaSdkSettings = this.imaSdkSettings;
if (imaSdkSettings == null) {
imaSdkSettings = ImaSdkFactory.getInstance().createImaSdkSettings();
imaSdkSettings.setLanguage(Util.getSystemLanguageCodes()[0]);
}
ImaUtil.ServerSideAdInsertionConfiguration configuration =
new ImaUtil.ServerSideAdInsertionConfiguration(
adViewProvider,
imaSdkSettings,
adEventListener,
adErrorListener,
companionAdSlots,
imaSdkSettings.isDebugMode());
return new AdsLoader(context, configuration);
}
}
private final ImaUtil.ServerSideAdInsertionConfiguration configuration;
private final Context context;
private final Map<ImaServerSideAdInsertionMediaSource, MediaSourceResourceHolder>
mediaSourceResources;
@Nullable private Player player;
private AdsLoader(Context context, ImaUtil.ServerSideAdInsertionConfiguration configuration) {
this.context = context.getApplicationContext();
this.configuration = configuration;
mediaSourceResources = new HashMap<>();
}
/**
* Sets the player.
*
* <p>This method needs to be called before adding server side ad insertion media items to the
* player.
*/
public void setPlayer(Player player) {
this.player = player;
}
public void addMediaSourceResources(
ImaServerSideAdInsertionMediaSource mediaSource,
StreamPlayer streamPlayer,
com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {
mediaSourceResources.put(mediaSource, new MediaSourceResourceHolder(streamPlayer, adsLoader));
}
/** Releases resources when the ads loader is no longer needed. */
public void release() {
for (MediaSourceResourceHolder resourceHolder : mediaSourceResources.values()) {
resourceHolder.streamPlayer.release();
resourceHolder.adsLoader.release();
}
mediaSourceResources.clear();
}
private static final class MediaSourceResourceHolder {
public final StreamPlayer streamPlayer;
public final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private MediaSourceResourceHolder(
StreamPlayer streamPlayer, com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {
this.streamPlayer = streamPlayer;
this.adsLoader = adsLoader;
}
}
}
private final MediaItem mediaItem;
private final Player player;
private final MediaSource.Factory contentMediaSourceFactory;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
@Nullable private final AdEventListener applicationAdEventListener;
@Nullable private final AdErrorListener applicationAdErrorListener;
private final ServerSideAdInsertionStreamRequest streamRequest;
private final StreamPlayer streamPlayer;
private final Handler mainHandler;
private final ComponentListener componentListener;
@Nullable private Loader loader;
@Nullable private StreamManager streamManager;
@Nullable private ServerSideAdInsertionMediaSource serverSideAdInsertionMediaSource;
@Nullable private IOException loadError;
private @MonotonicNonNull Timeline contentTimeline;
private AdPlaybackState adPlaybackState;
private ImaServerSideAdInsertionMediaSource(
MediaItem mediaItem,
Player player,
com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader,
StreamPlayer streamPlayer,
MediaSource.Factory contentMediaSourceFactory,
@Nullable AdEventListener applicationAdEventListener,
@Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener) {
this.mediaItem = mediaItem;
this.player = player;
this.adsLoader = adsLoader;
this.streamPlayer = streamPlayer;
this.contentMediaSourceFactory = contentMediaSourceFactory;
this.applicationAdEventListener = applicationAdEventListener;
this.applicationAdErrorListener = applicationAdErrorListener;
componentListener = new ComponentListener();
adPlaybackState = AdPlaybackState.NONE;
mainHandler = Util.createHandlerForCurrentLooper();
Uri streamRequestUri = checkNotNull(mediaItem.localConfiguration).uri;
streamRequest = ServerSideAdInsertionStreamRequest.fromUri(streamRequestUri);
}
@Override
public MediaItem getMediaItem() {
return mediaItem;
}
@Override
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
if (loader == null) {
Loader loader = new Loader("ImaServerSideAdInsertionMediaSource");
player.addListener(componentListener);
StreamManagerLoadable streamManagerLoadable =
new StreamManagerLoadable(
adsLoader,
streamRequest.getStreamRequest(),
streamPlayer,
applicationAdErrorListener,
streamRequest.loadVideoTimeoutMs);
loader.startLoading(
streamManagerLoadable,
new StreamManagerLoadableCallback(),
/* defaultMinRetryCount= */ 0);
this.loader = loader;
}
}
@Override
protected void onChildSourceInfoRefreshed(
Void id, MediaSource mediaSource, Timeline newTimeline) {
refreshSourceInfo(
new ForwardingTimeline(newTimeline) {
@Override
public Window getWindow(
int windowIndex, Window window, long defaultPositionProjectionUs) {
newTimeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
window.mediaItem = mediaItem;
return window;
}
});
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return checkNotNull(serverSideAdInsertionMediaSource)
.createPeriod(id, allocator, startPositionUs);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
checkNotNull(serverSideAdInsertionMediaSource).releasePeriod(mediaPeriod);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
super.maybeThrowSourceInfoRefreshError();
if (loadError != null) {
IOException loadError = this.loadError;
this.loadError = null;
throw loadError;
}
}
@Override
protected void releaseSourceInternal() {
super.releaseSourceInternal();
if (loader != null) {
loader.release();
player.removeListener(componentListener);
mainHandler.post(() -> setStreamManager(/* streamManager= */ null));
loader = null;
}
}
// Internal methods (called on the main thread).
@MainThread
private void setStreamManager(@Nullable StreamManager streamManager) {
if (this.streamManager == streamManager) {
return;
}
if (this.streamManager != null) {
if (applicationAdEventListener != null) {
this.streamManager.removeAdEventListener(applicationAdEventListener);
}
if (applicationAdErrorListener != null) {
this.streamManager.removeAdErrorListener(applicationAdErrorListener);
}
this.streamManager.removeAdEventListener(componentListener);
this.streamManager.destroy();
this.streamManager = null;
}
this.streamManager = streamManager;
if (streamManager != null) {
streamManager.addAdEventListener(componentListener);
if (applicationAdEventListener != null) {
streamManager.addAdEventListener(applicationAdEventListener);
}
if (applicationAdErrorListener != null) {
streamManager.addAdErrorListener(applicationAdErrorListener);
}
}
}
@MainThread
private void setAdPlaybackState(AdPlaybackState adPlaybackState) {
if (adPlaybackState.equals(this.adPlaybackState)) {
return;
}
this.adPlaybackState = adPlaybackState;
invalidateServerSideAdInsertionAdPlaybackState();
}
@MainThread
private void setContentTimeline(Timeline contentTimeline) {
if (contentTimeline.equals(this.contentTimeline)) {
return;
}
this.contentTimeline = contentTimeline;
invalidateServerSideAdInsertionAdPlaybackState();
}
@MainThread
private void invalidateServerSideAdInsertionAdPlaybackState() {
if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) {
ImmutableMap<Object, AdPlaybackState> splitAdPlaybackStates =
splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline);
streamPlayer.setAdPlaybackStates(streamRequest.adsId, splitAdPlaybackStates, contentTimeline);
checkNotNull(serverSideAdInsertionMediaSource).setAdPlaybackStates(splitAdPlaybackStates);
}
}
// Internal methods (called on the playback thread).
private void setContentUri(Uri contentUri) {
if (serverSideAdInsertionMediaSource != null) {
return;
}
ServerSideAdInsertionMediaSource serverSideAdInsertionMediaSource =
new ServerSideAdInsertionMediaSource(
contentMediaSourceFactory.createMediaSource(MediaItem.fromUri(contentUri)),
componentListener);
this.serverSideAdInsertionMediaSource = serverSideAdInsertionMediaSource;
if (streamRequest.isLiveStream()) {
AdPlaybackState liveAdPlaybackState =
new AdPlaybackState(streamRequest.adsId)
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
mainHandler.post(() -> setAdPlaybackState(liveAdPlaybackState));
}
prepareChildSource(/* id= */ null, serverSideAdInsertionMediaSource);
}
// Static methods.
private static AdPlaybackState setVodAdGroupPlaceholders(
List<CuePoint> cuePoints, AdPlaybackState adPlaybackState) {
for (int i = 0; i < cuePoints.size(); i++) {
CuePoint cuePoint = cuePoints.get(i);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ secToUs(cuePoint.getStartTime()),
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ secToUs(cuePoint.getEndTime() - cuePoint.getStartTime()));
}
return adPlaybackState;
}
private static AdPlaybackState setVodAdInPlaceholder(Ad ad, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
// Handle post rolls that have a podIndex of -1.
int adGroupIndex =
adPodInfo.getPodIndex() == -1 ? adPlaybackState.adGroupCount - 1 : adPodInfo.getPodIndex();
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
if (adGroup.count < adPodInfo.getTotalAds()) {
adPlaybackState =
expandAdGroupPlaceholder(
adGroupIndex,
/* adGroupDurationUs= */ secToUs(adPodInfo.getMaxDuration()),
adIndexInAdGroup,
/* adDurationUs= */ secToUs(ad.getDuration()),
/* adsInAdGroupCount= */ adPodInfo.getTotalAds(),
adPlaybackState);
} else if (adIndexInAdGroup < adGroup.count - 1) {
adPlaybackState =
updateAdDurationInAdGroup(
adGroupIndex,
adIndexInAdGroup,
/* adDurationUs= */ secToUs(ad.getDuration()),
adPlaybackState);
}
return adPlaybackState;
}
private static AdPlaybackState addLiveAdBreak(
Ad ad, long currentPeriodPositionUs, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
long adDurationUs = secToUs(ad.getDuration());
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
// TODO(b/208398934) Support seeking backwards.
if (adIndexInAdGroup == 0 || adPlaybackState.adGroupCount == 1) {
// First ad of group. Create a new group with all ads.
long[] adDurationsUs =
updateAdDurationAndPropagate(
new long[adPodInfo.getTotalAds()],
adIndexInAdGroup,
adDurationUs,
secToUs(adPodInfo.getMaxDuration()));
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ currentPeriodPositionUs,
/* contentResumeOffsetUs= */ sum(adDurationsUs),
/* adDurationsUs...= */ adDurationsUs);
} else {
int adGroupIndex = adPlaybackState.adGroupCount - 2;
adPlaybackState =
updateAdDurationInAdGroup(adGroupIndex, adIndexInAdGroup, adDurationUs, adPlaybackState);
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
return adPlaybackState.withContentResumeOffsetUs(
adGroupIndex, min(adGroup.contentResumeOffsetUs, sum(adGroup.durationsUs)));
}
return adPlaybackState;
}
private static AdPlaybackState skipAd(Ad ad, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
int adGroupIndex = adPodInfo.getPodIndex();
// IMA SDK always returns index starting at 1.
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
return adPlaybackState.withSkippedAd(adGroupIndex, adIndexInAdGroup);
}
private final class ComponentListener
implements AdEvent.AdEventListener, Player.Listener, AdPlaybackStateUpdater {
// Implement Player.Listener.
@Override
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (reason != Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
// Only auto transitions within the same or to the next media item are of interest.
return;
}
if (mediaItem.equals(oldPosition.mediaItem) && !mediaItem.equals(newPosition.mediaItem)) {
// Playback automatically transitioned to the next media item. Notify the SDK.
streamPlayer.onContentCompleted();
}
if (!mediaItem.equals(oldPosition.mediaItem)
|| !mediaItem.equals(newPosition.mediaItem)
|| !streamRequest.adsId.equals(
player
.getCurrentTimeline()
.getPeriodByUid(checkNotNull(newPosition.periodUid), new Timeline.Period())
.getAdsId())) {
// Discontinuity not within this ad media source.
return;
}
if (oldPosition.adGroupIndex != C.INDEX_UNSET && newPosition.adGroupIndex == C.INDEX_UNSET) {
AdPlaybackState newAdPlaybackState = adPlaybackState;
for (int i = 0; i <= oldPosition.adIndexInAdGroup; i++) {
int state = newAdPlaybackState.getAdGroup(oldPosition.adGroupIndex).states[i];
if (state != AdPlaybackState.AD_STATE_SKIPPED
&& state != AdPlaybackState.AD_STATE_ERROR) {
newAdPlaybackState =
newAdPlaybackState.withPlayedAd(
oldPosition.adGroupIndex, /* adIndexInAdGroup= */ i);
}
}
setAdPlaybackState(newAdPlaybackState);
}
}
@Override
public void onMetadata(Metadata metadata) {
if (!isCurrentAdPlaying(player, mediaItem, streamRequest.adsId)) {
return;
}
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)) {
streamPlayer.triggerUserTextReceived(textFrame.value);
}
} else if (entry instanceof EventMessage) {
EventMessage eventMessage = (EventMessage) entry;
String eventMessageValue = new String(eventMessage.messageData);
streamPlayer.triggerUserTextReceived(eventMessageValue);
}
}
}
@Override
public void onPlaybackStateChanged(@Player.State int state) {
if (state == Player.STATE_ENDED
&& isCurrentAdPlaying(player, mediaItem, streamRequest.adsId)) {
streamPlayer.onContentCompleted();
}
}
@Override
public void onVolumeChanged(float volume) {
if (!isCurrentAdPlaying(player, mediaItem, streamRequest.adsId)) {
return;
}
int volumePct = (int) Math.floor(volume * 100);
streamPlayer.onContentVolumeChanged(volumePct);
}
// Implement AdEvent.AdEventListener.
@MainThread
@Override
public void onAdEvent(AdEvent event) {
AdPlaybackState newAdPlaybackState = adPlaybackState;
switch (event.getType()) {
case CUEPOINTS_CHANGED:
// CUEPOINTS_CHANGED event is firing multiple times with the same queue points.
if (!streamRequest.isLiveStream() && newAdPlaybackState.equals(AdPlaybackState.NONE)) {
newAdPlaybackState =
setVodAdGroupPlaceholders(
checkNotNull(streamManager).getCuePoints(),
new AdPlaybackState(streamRequest.adsId));
}
break;
case LOADED:
if (streamRequest.isLiveStream()) {
Timeline timeline = player.getCurrentTimeline();
Timeline.Window window =
timeline.getWindow(player.getCurrentMediaItemIndex(), new Timeline.Window());
if (window.lastPeriodIndex > window.firstPeriodIndex) {
// multi-period live not integrated
return;
}
long positionInWindowUs =
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.positionInWindowUs;
long currentPeriodPosition =
Util.msToUs(player.getCurrentPosition()) - positionInWindowUs;
newAdPlaybackState =
addLiveAdBreak(
event.getAd(),
currentPeriodPosition,
newAdPlaybackState.equals(AdPlaybackState.NONE)
? new AdPlaybackState(streamRequest.adsId)
: newAdPlaybackState);
} else {
newAdPlaybackState = setVodAdInPlaceholder(event.getAd(), newAdPlaybackState);
}
break;
case SKIPPED:
if (!streamRequest.isLiveStream()) {
newAdPlaybackState = skipAd(event.getAd(), newAdPlaybackState);
}
break;
default:
// Do nothing.
break;
}
setAdPlaybackState(newAdPlaybackState);
}
// Implement AdPlaybackStateUpdater (called on the playback thread).
@Override
public boolean onAdPlaybackStateUpdateRequested(Timeline contentTimeline) {
mainHandler.post(() -> setContentTimeline(contentTimeline));
// Defer source refresh to ad playback state update for VOD. Refresh immediately when live
// with single period.
return !streamRequest.isLiveStream() || contentTimeline.getPeriodCount() > 1;
}
}
private final class StreamManagerLoadableCallback
implements Loader.Callback<StreamManagerLoadable> {
@Override
public void onLoadCompleted(
StreamManagerLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
mainHandler.post(() -> setStreamManager(checkNotNull(loadable.getStreamManager())));
setContentUri(checkNotNull(loadable.getContentUri()));
}
@Override
public void onLoadCanceled(
StreamManagerLoadable loadable,
long elapsedRealtimeMs,
long loadDurationMs,
boolean released) {
// We only cancel when the loader is released.
checkState(released);
}
@Override
public LoadErrorAction onLoadError(
StreamManagerLoadable loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error,
int errorCount) {
loadError = error;
return Loader.DONT_RETRY;
}
}
/** Loads the {@link StreamManager} and the content URI. */
private static class StreamManagerLoadable
implements Loadable, AdsLoadedListener, AdErrorListener {
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private final StreamRequest request;
private final StreamPlayer streamPlayer;
@Nullable private final AdErrorListener adErrorListener;
private final int loadVideoTimeoutMs;
private final ConditionVariable conditionVariable;
@Nullable private volatile StreamManager streamManager;
@Nullable private volatile Uri contentUri;
private volatile boolean cancelled;
private volatile boolean error;
@Nullable private volatile String errorMessage;
private volatile int errorCode;
/** Creates an instance. */
private StreamManagerLoadable(
com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader,
StreamRequest request,
StreamPlayer streamPlayer,
@Nullable AdErrorListener adErrorListener,
int loadVideoTimeoutMs) {
this.adsLoader = adsLoader;
this.request = request;
this.streamPlayer = streamPlayer;
this.adErrorListener = adErrorListener;
this.loadVideoTimeoutMs = loadVideoTimeoutMs;
conditionVariable = new ConditionVariable();
errorCode = -1;
}
/** Returns the DAI content URI or null if not yet available. */
@Nullable
public Uri getContentUri() {
return contentUri;
}
/** Returns the stream manager or null if not yet loaded. */
@Nullable
public StreamManager getStreamManager() {
return streamManager;
}
// Implement Loadable.
@Override
public void load() throws IOException {
try {
// SDK will call loadUrl on stream player for SDK once manifest uri is available.
streamPlayer.setStreamLoadListener(
(streamUri, subtitles) -> {
contentUri = Uri.parse(streamUri);
conditionVariable.open();
});
if (adErrorListener != null) {
adsLoader.addAdErrorListener(adErrorListener);
}
adsLoader.addAdsLoadedListener(this);
adsLoader.addAdErrorListener(this);
adsLoader.requestStream(request);
while (contentUri == null && !cancelled && !error) {
try {
conditionVariable.block();
} catch (InterruptedException e) {
/* Do nothing. */
}
}
if (error && contentUri == null) {
throw new IOException(errorMessage + " [errorCode: " + errorCode + "]");
}
} finally {
adsLoader.removeAdsLoadedListener(this);
adsLoader.removeAdErrorListener(this);
if (adErrorListener != null) {
adsLoader.removeAdErrorListener(adErrorListener);
}
}
}
@Override
public void cancelLoad() {
cancelled = true;
}
// AdsLoader.AdsLoadedListener implementation.
@MainThread
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent event) {
StreamManager streamManager = event.getStreamManager();
if (streamManager == null) {
error = true;
errorMessage = "streamManager is null after ads manager has been loaded";
conditionVariable.open();
return;
}
AdsRenderingSettings adsRenderingSettings =
ImaSdkFactory.getInstance().createAdsRenderingSettings();
adsRenderingSettings.setLoadVideoTimeout(loadVideoTimeoutMs);
// After initialization completed the streamUri will be reported to the streamPlayer.
streamManager.init(adsRenderingSettings);
this.streamManager = streamManager;
}
// AdErrorEvent.AdErrorListener implementation.
@MainThread
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
error = true;
if (adErrorEvent.getError() != null) {
@Nullable String errorMessage = adErrorEvent.getError().getMessage();
if (errorMessage != null) {
this.errorMessage = errorMessage.replace('\n', ' ');
}
errorCode = adErrorEvent.getError().getErrorCodeNumber();
}
conditionVariable.open();
}
}
/**
* Receives the content URI from the SDK and sends back in-band media metadata and playback
* progression data to the SDK.
*/
private static final class StreamPlayer implements VideoStreamPlayer {
/** A listener to listen for the stream URI loaded by the SDK. */
public interface StreamLoadListener {
/**
* Loads a stream with dynamic ad insertion given the stream url and subtitles array. The
* subtitles array is only used in VOD streams.
*
* <p>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:
*
* <p>"language" -> "en" "language_name" -> "English" "webvtt" ->
* "https://example.com/vtt/en.vtt" "ttml" -> "https://example.com/ttml/en.ttml"
*/
void onLoadStream(String streamUri, List<HashMap<String, String>> subtitles);
}
private final List<VideoStreamPlayer.VideoStreamPlayerCallback> callbacks;
private final Player player;
private final MediaItem mediaItem;
private final Timeline.Window window;
private final Timeline.Period period;
private ImmutableMap<Object, AdPlaybackState> adPlaybackStates;
@Nullable private Timeline contentTimeline;
@Nullable private Object adsId;
@Nullable private StreamLoadListener streamLoadListener;
/** Creates an instance. */
public StreamPlayer(Player player, MediaItem mediaItem) {
this.player = player;
this.mediaItem = mediaItem;
callbacks = new ArrayList<>(/* initialCapacity= */ 1);
adPlaybackStates = ImmutableMap.of();
window = new Timeline.Window();
period = new Timeline.Period();
}
/** Registers the ad playback states matching to the given content timeline. */
public void setAdPlaybackStates(
Object adsId,
ImmutableMap<Object, AdPlaybackState> adPlaybackStates,
Timeline contentTimeline) {
this.adsId = adsId;
this.adPlaybackStates = adPlaybackStates;
this.contentTimeline = contentTimeline;
}
/** Sets the {@link StreamLoadListener} to be called when the SSAI content URI was loaded. */
public void setStreamLoadListener(StreamLoadListener listener) {
streamLoadListener = Assertions.checkNotNull(listener);
}
/** Called when the content has completed playback. */
public void onContentCompleted() {
for (VideoStreamPlayer.VideoStreamPlayerCallback callback : callbacks) {
callback.onContentComplete();
}
}
/** Called when the content player changed the volume. */
public void onContentVolumeChanged(int volumePct) {
for (VideoStreamPlayer.VideoStreamPlayerCallback callback : callbacks) {
callback.onVolumeChanged(volumePct);
}
}
/** Releases the player. */
public void release() {
callbacks.clear();
adsId = null;
adPlaybackStates = ImmutableMap.of();
contentTimeline = null;
streamLoadListener = null;
}
// Implements VolumeProvider.
@Override
public int getVolume() {
return (int) Math.floor(player.getVolume() * 100);
}
// Implement ContentProgressProvider.
@Override
public VideoProgressUpdate getContentProgress() {
if (!isCurrentAdPlaying(player, mediaItem, adsId)) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} else if (adPlaybackStates.isEmpty()) {
return new VideoProgressUpdate(/* currentTimeMs= */ 0, /* durationMs= */ C.TIME_UNSET);
}
Timeline timeline = player.getCurrentTimeline();
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 =
checkNotNull(contentTimeline)
.getPeriod(
currentPeriodIndex - window.firstPeriodIndex,
new Timeline.Period(),
/* 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);
}
return new VideoProgressUpdate(
streamPositionMs,
checkNotNull(contentTimeline).getWindow(/* windowIndex= */ 0, window).getDurationMs());
}
// Implement VideoStreamPlayer.
@Override
public void loadUrl(String url, List<HashMap<String, String>> 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) {
// Do nothing.
}
// Internal methods.
private void triggerUserTextReceived(String userText) {
for (VideoStreamPlayer.VideoStreamPlayerCallback callback : callbacks) {
callback.onUserTextReceived(userText);
}
}
}
private static boolean isCurrentAdPlaying(
Player player, MediaItem mediaItem, @Nullable Object adsId) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
return false;
}
Timeline.Period period = new Timeline.Period();
player.getCurrentTimeline().getPeriod(player.getCurrentPeriodIndex(), period);
return (period.isPlaceholder && mediaItem.equals(player.getCurrentMediaItem()))
|| (adsId != null && adsId.equals(period.getAdsId()));
}
private static StreamDisplayContainer createStreamDisplayContainer(
ImaSdkFactory imaSdkFactory,
ImaUtil.ServerSideAdInsertionConfiguration config,
StreamPlayer streamPlayer) {
StreamDisplayContainer container =
ImaSdkFactory.createStreamDisplayContainer(
checkNotNull(config.adViewProvider.getAdViewGroup()), streamPlayer);
container.setCompanionSlots(config.companionAdSlots);
registerFriendlyObstructions(imaSdkFactory, container, config.adViewProvider);
return container;
}
private static void registerFriendlyObstructions(
ImaSdkFactory imaSdkFactory,
StreamDisplayContainer container,
AdViewProvider adViewProvider) {
for (int i = 0; i < adViewProvider.getAdOverlayInfos().size(); i++) {
AdOverlayInfo overlayInfo = adViewProvider.getAdOverlayInfos().get(i);
container.registerFriendlyObstruction(
imaSdkFactory.createFriendlyObstruction(
overlayInfo.view,
ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose),
overlayInfo.reasonDetail != null ? overlayInfo.reasonDetail : "Unknown reason"));
}
}
}
/*
* 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 com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.StreamRequest;
import com.google.ads.interactivemedia.v3.api.StreamRequest.StreamFormat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import java.util.Map;
/** Stream request data for an IMA DAI stream. */
/* package */ final class ServerSideAdInsertionStreamRequest {
/** The default timeout for loading the video URI, in milliseconds. */
public static final int DEFAULT_LOAD_VIDEO_TIMEOUT_MS = 10_000;
/** Builds a {@link ServerSideAdInsertionStreamRequest}. */
public static final class Builder {
@Nullable private String adsId;
@Nullable private String assetKey;
@Nullable private String apiKey;
@Nullable private String contentSourceId;
@Nullable private String videoId;
@Nullable private String manifestSuffix;
@Nullable private String contentUrl;
@Nullable private String authToken;
@Nullable private String streamActivityMonitorId;
private ImmutableMap<String, String> adTagParameters;
@ContentType public int format = C.TYPE_HLS;
private int loadVideoTimeoutMs;
/** Creates a new instance. */
public Builder() {
adTagParameters = ImmutableMap.of();
loadVideoTimeoutMs = DEFAULT_LOAD_VIDEO_TIMEOUT_MS;
}
/**
* An opaque identifier for associated ad playback state, or {@code null} if the {@link
* #setAssetKey(String) asset key} (for live) or {@link #setVideoId(String) video id} (for VOD)
* should be used as the ads identifier.
*
* @param adsId The ads identifier.
* @return This instance, for convenience.
*/
public Builder setAdsId(String adsId) {
this.adsId = adsId;
return this;
}
/**
* The stream request asset key used for live streams.
*
* @param assetKey Live stream asset key.
* @return This instance, for convenience.
*/
public Builder setAssetKey(@Nullable String assetKey) {
this.assetKey = assetKey;
return this;
}
/**
* Sets the stream request authorization token. Used in place of {@link #setApiKey(String) the
* API key} for stricter content authorization. The publisher can control individual content
* streams authorizations based on this token.
*
* @param authToken Live stream authorization token.
* @return This instance, for convenience.
*/
public Builder setAuthToken(@Nullable String authToken) {
this.authToken = authToken;
return this;
}
/**
* The stream request content source ID used for on-demand streams.
*
* @param contentSourceId VOD stream content source id.
* @return This instance, for convenience.
*/
public Builder setContentSourceId(@Nullable String contentSourceId) {
this.contentSourceId = contentSourceId;
return this;
}
/**
* The stream request video ID used for on-demand streams.
*
* @param videoId VOD stream video id.
* @return This instance, for convenience.
*/
public Builder setVideoId(@Nullable String videoId) {
this.videoId = videoId;
return this;
}
/**
* Sets the format of the stream request.
*
* @param format VOD or live stream type.
* @return This instance, for convenience.
*/
public Builder setFormat(@ContentType int format) {
checkArgument(format == C.TYPE_DASH || format == C.TYPE_HLS);
this.format = format;
return this;
}
/**
* The stream request API key. This is used for content authentication. The API key is provided
* to the publisher to unlock their content. It's a security measure used to verify the
* applications that are attempting to access the content.
*
* @param apiKey Stream api key.
* @return This instance, for convenience.
*/
public Builder setApiKey(@Nullable String apiKey) {
this.apiKey = apiKey;
return this;
}
/**
* Sets the ID to be used to debug the stream with the stream activity monitor. This is used to
* provide a convenient way to allow publishers to find a stream log in the stream activity
* monitor tool.
*
* @param streamActivityMonitorId ID for debugging the stream with the stream activity monitor.
* @return This instance, for convenience.
*/
public Builder setStreamActivityMonitorId(@Nullable String streamActivityMonitorId) {
this.streamActivityMonitorId = streamActivityMonitorId;
return this;
}
/**
* Sets the overridable ad tag parameters on the stream request. <a
* href="//support.google.com/dfp_premium/answer/7320899">Supply targeting parameters to your
* stream</a> provides more information.
*
* <p>You can use the dai-ot and dai-ov parameters for stream variant preference. See <a
* href="//support.google.com/dfp_premium/answer/7320898">Override Stream Variant Parameters</a>
* for more information.
*
* @param adTagParameters A map of extra parameters to pass to the ad server.
* @return This instance, for convenience.
*/
public Builder setAdTagParameters(Map<String, String> adTagParameters) {
this.adTagParameters = ImmutableMap.copyOf(adTagParameters);
return this;
}
/**
* Sets the optional stream manifest's suffix, which will be appended to the stream manifest's
* URL. The provided string must be URL-encoded and must not include a leading question mark.
*
* @param manifestSuffix Stream manifest's suffix.
* @return This instance, for convenience.
*/
public Builder setManifestSuffix(@Nullable String manifestSuffix) {
this.manifestSuffix = manifestSuffix;
return this;
}
/**
* Specifies the deep link to the content's screen. If provided, this parameter is passed to the
* OM SDK. See <a href="//developer.android.com/training/app-links/deep-linking">Android
* documentation</a> for more information.
*
* @param contentUrl Deep link to the content's screen.
* @return This instance, for convenience.
*/
public Builder setContentUrl(@Nullable String contentUrl) {
this.contentUrl = contentUrl;
return this;
}
/**
* Sets the duration after which resolving the video URI should time out, in milliseconds.
*
* <p>The default is {@link #DEFAULT_LOAD_VIDEO_TIMEOUT_MS} milliseconds.
*
* @param loadVideoTimeoutMs The timeout after which to give up resolving the video URI.
* @return This instance, for convenience.
*/
public Builder setLoadVideoTimeoutMs(int loadVideoTimeoutMs) {
this.loadVideoTimeoutMs = loadVideoTimeoutMs;
return this;
}
/**
* Builds a {@link ServerSideAdInsertionStreamRequest} with the builder's current values.
*
* @return The build {@link ServerSideAdInsertionStreamRequest}.
* @throws IllegalStateException If request has missing or invalid inputs.
*/
public ServerSideAdInsertionStreamRequest build() {
checkState(
(TextUtils.isEmpty(assetKey)
&& !TextUtils.isEmpty(contentSourceId)
&& !TextUtils.isEmpty(videoId))
|| (!TextUtils.isEmpty(assetKey)
&& TextUtils.isEmpty(contentSourceId)
&& TextUtils.isEmpty(videoId)));
@Nullable String adsId = this.adsId;
if (adsId == null) {
adsId = assetKey != null ? assetKey : checkNotNull(videoId);
}
return new ServerSideAdInsertionStreamRequest(
adsId,
assetKey,
apiKey,
contentSourceId,
videoId,
adTagParameters,
manifestSuffix,
contentUrl,
authToken,
streamActivityMonitorId,
format,
loadVideoTimeoutMs);
}
}
private static final String SCHEME = "imadai";
private static final String ADS_ID = "adsId";
private static final String ASSET_KEY = "assetKey";
private static final String API_KEY = "apiKey";
private static final String CONTENT_SOURCE_ID = "contentSourceId";
private static final String VIDEO_ID = "videoId";
private static final String AD_TAG_PARAMETERS = "adTagParameters";
private static final String MANIFEST_SUFFIX = "manifestSuffix";
private static final String CONTENT_URL = "contentUrl";
private static final String AUTH_TOKEN = "authToken";
private static final String STREAM_ACTIVITY_MONITOR_ID = "streamActivityMonitorId";
private static final String FORMAT = "format";
private static final String LOAD_VIDEO_TIMEOUT_MS = "loadVideoTimeoutMs";
public final String adsId;
@Nullable public final String assetKey;
@Nullable public final String apiKey;
@Nullable public final String contentSourceId;
@Nullable public final String videoId;
public final ImmutableMap<String, String> adTagParameters;
@Nullable public final String manifestSuffix;
@Nullable public final String contentUrl;
@Nullable public final String authToken;
@Nullable public final String streamActivityMonitorId;
@ContentType public int format = C.TYPE_HLS;
public final int loadVideoTimeoutMs;
private ServerSideAdInsertionStreamRequest(
String adsId,
@Nullable String assetKey,
@Nullable String apiKey,
@Nullable String contentSourceId,
@Nullable String videoId,
ImmutableMap<String, String> adTagParameters,
@Nullable String manifestSuffix,
@Nullable String contentUrl,
@Nullable String authToken,
@Nullable String streamActivityMonitorId,
@ContentType int format,
int loadVideoTimeoutMs) {
this.adsId = adsId;
this.assetKey = assetKey;
this.apiKey = apiKey;
this.contentSourceId = contentSourceId;
this.videoId = videoId;
this.adTagParameters = adTagParameters;
this.manifestSuffix = manifestSuffix;
this.contentUrl = contentUrl;
this.authToken = authToken;
this.streamActivityMonitorId = streamActivityMonitorId;
this.format = format;
this.loadVideoTimeoutMs = loadVideoTimeoutMs;
}
/** Returns whether this request is for a live stream or false if it is a VOD stream. */
public boolean isLiveStream() {
return !TextUtils.isEmpty(assetKey);
}
/** Returns the corresponding {@link StreamRequest}. */
@SuppressWarnings("nullness") // Required for making nullness test pass for library_with_ima_sdk.
public StreamRequest getStreamRequest() {
StreamRequest streamRequest;
if (!TextUtils.isEmpty(assetKey)) {
streamRequest = ImaSdkFactory.getInstance().createLiveStreamRequest(assetKey, apiKey);
} else {
streamRequest =
ImaSdkFactory.getInstance()
.createVodStreamRequest(checkNotNull(contentSourceId), checkNotNull(videoId), apiKey);
}
if (format == C.TYPE_DASH) {
streamRequest.setFormat(StreamFormat.DASH);
} else if (format == C.TYPE_HLS) {
streamRequest.setFormat(StreamFormat.HLS);
}
// Optional params.
streamRequest.setAdTagParameters(adTagParameters);
if (manifestSuffix != null) {
streamRequest.setManifestSuffix(manifestSuffix);
}
if (contentUrl != null) {
streamRequest.setContentUrl(contentUrl);
}
if (authToken != null) {
streamRequest.setAuthToken(authToken);
}
if (streamActivityMonitorId != null) {
streamRequest.setStreamActivityMonitorId(streamActivityMonitorId);
}
return streamRequest;
}
/** Returns a corresponding {@link Uri}. */
public Uri toUri() {
Uri.Builder dataUriBuilder = new Uri.Builder();
dataUriBuilder.scheme(SCHEME);
dataUriBuilder.appendQueryParameter(ADS_ID, adsId);
if (loadVideoTimeoutMs != DEFAULT_LOAD_VIDEO_TIMEOUT_MS) {
dataUriBuilder.appendQueryParameter(
LOAD_VIDEO_TIMEOUT_MS, String.valueOf(loadVideoTimeoutMs));
}
if (assetKey != null) {
dataUriBuilder.appendQueryParameter(ASSET_KEY, assetKey);
}
if (apiKey != null) {
dataUriBuilder.appendQueryParameter(API_KEY, apiKey);
}
if (contentSourceId != null) {
dataUriBuilder.appendQueryParameter(CONTENT_SOURCE_ID, contentSourceId);
}
if (videoId != null) {
dataUriBuilder.appendQueryParameter(VIDEO_ID, videoId);
}
if (manifestSuffix != null) {
dataUriBuilder.appendQueryParameter(MANIFEST_SUFFIX, manifestSuffix);
}
if (contentUrl != null) {
dataUriBuilder.appendQueryParameter(CONTENT_URL, contentUrl);
}
if (authToken != null) {
dataUriBuilder.appendQueryParameter(AUTH_TOKEN, authToken);
}
if (streamActivityMonitorId != null) {
dataUriBuilder.appendQueryParameter(STREAM_ACTIVITY_MONITOR_ID, streamActivityMonitorId);
}
if (!adTagParameters.isEmpty()) {
Uri.Builder adTagParametersUriBuilder = new Uri.Builder();
for (Map.Entry<String, String> entry : adTagParameters.entrySet()) {
adTagParametersUriBuilder.appendQueryParameter(entry.getKey(), entry.getValue());
}
dataUriBuilder.appendQueryParameter(
AD_TAG_PARAMETERS, adTagParametersUriBuilder.build().toString());
}
dataUriBuilder.appendQueryParameter(FORMAT, String.valueOf(format));
return dataUriBuilder.build();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ServerSideAdInsertionStreamRequest)) {
return false;
}
ServerSideAdInsertionStreamRequest that = (ServerSideAdInsertionStreamRequest) o;
return format == that.format
&& loadVideoTimeoutMs == that.loadVideoTimeoutMs
&& Objects.equal(adsId, that.adsId)
&& Objects.equal(assetKey, that.assetKey)
&& Objects.equal(apiKey, that.apiKey)
&& Objects.equal(contentSourceId, that.contentSourceId)
&& Objects.equal(videoId, that.videoId)
&& Objects.equal(adTagParameters, that.adTagParameters)
&& Objects.equal(manifestSuffix, that.manifestSuffix)
&& Objects.equal(contentUrl, that.contentUrl)
&& Objects.equal(authToken, that.authToken)
&& Objects.equal(streamActivityMonitorId, that.streamActivityMonitorId);
}
@Override
public int hashCode() {
return Objects.hashCode(
adsId,
assetKey,
apiKey,
contentSourceId,
videoId,
adTagParameters,
manifestSuffix,
contentUrl,
authToken,
streamActivityMonitorId,
loadVideoTimeoutMs,
format);
}
/**
* Creates a {@link ServerSideAdInsertionStreamRequest} for the given URI.
*
* @param uri The URI.
* @return An {@link ServerSideAdInsertionStreamRequest} for the given URI.
* @throws IllegalStateException If uri has missing or invalid inputs.
*/
public static ServerSideAdInsertionStreamRequest fromUri(Uri uri) {
ServerSideAdInsertionStreamRequest.Builder request =
new ServerSideAdInsertionStreamRequest.Builder();
if (!SCHEME.equals(uri.getScheme())) {
throw new IllegalArgumentException("Invalid scheme.");
}
request.setAdsId(checkNotNull(uri.getQueryParameter(ADS_ID)));
request.setAssetKey(uri.getQueryParameter(ASSET_KEY));
request.setApiKey(uri.getQueryParameter(API_KEY));
request.setContentSourceId(uri.getQueryParameter(CONTENT_SOURCE_ID));
request.setVideoId(uri.getQueryParameter(VIDEO_ID));
request.setManifestSuffix(uri.getQueryParameter(MANIFEST_SUFFIX));
request.setContentUrl(uri.getQueryParameter(CONTENT_URL));
request.setAuthToken(uri.getQueryParameter(AUTH_TOKEN));
request.setStreamActivityMonitorId(uri.getQueryParameter(STREAM_ACTIVITY_MONITOR_ID));
String adsLoaderTimeoutUs = uri.getQueryParameter(LOAD_VIDEO_TIMEOUT_MS);
request.setLoadVideoTimeoutMs(
TextUtils.isEmpty(adsLoaderTimeoutUs)
? DEFAULT_LOAD_VIDEO_TIMEOUT_MS
: Integer.parseInt(adsLoaderTimeoutUs));
String formatValue = uri.getQueryParameter(FORMAT);
if (!TextUtils.isEmpty(formatValue)) {
request.setFormat(Integer.parseInt(formatValue));
}
Map<String, String> adTagParameters;
String adTagParametersValue;
String singleAdTagParameterValue;
if (uri.getQueryParameter(AD_TAG_PARAMETERS) != null) {
adTagParameters = new HashMap<>();
adTagParametersValue = uri.getQueryParameter(AD_TAG_PARAMETERS);
if (!TextUtils.isEmpty(adTagParametersValue)) {
Uri adTagParametersUri = Uri.parse(adTagParametersValue);
for (String paramName : adTagParametersUri.getQueryParameterNames()) {
singleAdTagParameterValue = adTagParametersUri.getQueryParameter(paramName);
if (!TextUtils.isEmpty(singleAdTagParameterValue)) {
adTagParameters.put(paramName, singleAdTagParameterValue);
}
}
}
request.setAdTagParameters(adTagParameters);
}
return request.build();
}
}
/*
* Copyright 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 com.google.android.exoplayer2.ext.ima;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link ServerSideAdInsertionStreamRequest}. */
@RunWith(AndroidJUnit4.class)
public final class ServerSideAdInsertionStreamRequestTest {
private static final String ADS_ID = "testAdsId";
private static final String ASSET_KEY = "testAssetKey";
private static final String API_KEY = "testApiKey";
private static final String CONTENT_SOURCE_ID = "testContentSourceId";
private static final String VIDEO_ID = "testVideoId";
private static final String MANIFEST_SUFFIX = "testManifestSuffix";
private static final String CONTENT_URL =
"http://google.com/contentUrl?queryParamName=queryParamValue";
private static final String AUTH_TOKEN = "testAuthToken";
private static final String STREAM_ACTIVITY_MONITOR_ID = "testStreamActivityMonitorId";
private static final int ADS_LOADER_TIMEOUT_MS = 2;
private static final int FORMAT_DASH = 0;
private static final int FORMAT_HLS = 2;
private static final Map<String, String> adTagParameters = new HashMap<>();
static {
adTagParameters.put("param1", "value1");
adTagParameters.put("param2", "value2");
}
@Test
public void build_live_correctUriAndParsing() {
ServerSideAdInsertionStreamRequest.Builder builder =
new ServerSideAdInsertionStreamRequest.Builder();
builder.setAdsId(ADS_ID);
builder.setAssetKey(ASSET_KEY);
builder.setApiKey(API_KEY);
builder.setManifestSuffix(MANIFEST_SUFFIX);
builder.setContentUrl(CONTENT_URL);
builder.setAuthToken(AUTH_TOKEN);
builder.setStreamActivityMonitorId(STREAM_ACTIVITY_MONITOR_ID);
builder.setFormat(FORMAT_HLS);
builder.setAdTagParameters(adTagParameters);
builder.setLoadVideoTimeoutMs(ADS_LOADER_TIMEOUT_MS);
ServerSideAdInsertionStreamRequest streamRequest = builder.build();
ServerSideAdInsertionStreamRequest requestAfterConversions =
ServerSideAdInsertionStreamRequest.fromUri(streamRequest.toUri());
assertThat(streamRequest).isEqualTo(requestAfterConversions);
}
@Test
public void build_vod_correctUriAndParsing() {
ServerSideAdInsertionStreamRequest.Builder builder =
new ServerSideAdInsertionStreamRequest.Builder();
builder.setAdsId(ADS_ID);
builder.setApiKey(API_KEY);
builder.setContentSourceId(CONTENT_SOURCE_ID);
builder.setVideoId(VIDEO_ID);
builder.setManifestSuffix(MANIFEST_SUFFIX);
builder.setContentUrl(CONTENT_URL);
builder.setAuthToken(AUTH_TOKEN);
builder.setStreamActivityMonitorId(STREAM_ACTIVITY_MONITOR_ID);
builder.setFormat(FORMAT_DASH);
builder.setAdTagParameters(adTagParameters);
builder.setLoadVideoTimeoutMs(ADS_LOADER_TIMEOUT_MS);
ServerSideAdInsertionStreamRequest streamRequest = builder.build();
ServerSideAdInsertionStreamRequest requestAfterConversions =
ServerSideAdInsertionStreamRequest.fromUri(streamRequest.toUri());
assertThat(requestAfterConversions).isEqualTo(streamRequest);
}
@Test
public void build_vodWithNoAdsId_usesVideoIdAsDefault() {
ServerSideAdInsertionStreamRequest.Builder builder =
new ServerSideAdInsertionStreamRequest.Builder();
builder.setContentSourceId(CONTENT_SOURCE_ID);
builder.setVideoId(VIDEO_ID);
ServerSideAdInsertionStreamRequest streamRequest = builder.build();
assertThat(streamRequest.adsId).isEqualTo(VIDEO_ID);
assertThat(streamRequest.toUri().getQueryParameter("adsId")).isEqualTo(VIDEO_ID);
}
@Test
public void build_liveWithNoAdsId_usesAssetKeyAsDefault() {
ServerSideAdInsertionStreamRequest.Builder builder =
new ServerSideAdInsertionStreamRequest.Builder();
builder.setAssetKey(ASSET_KEY);
ServerSideAdInsertionStreamRequest streamRequest = builder.build();
assertThat(streamRequest.adsId).isEqualTo(ASSET_KEY);
assertThat(streamRequest.toUri().getQueryParameter("adsId")).isEqualTo(ASSET_KEY);
}
@Test
public void build_assetKeyWithVideoId_throwsIllegalStateException() {
ServerSideAdInsertionStreamRequest.Builder requestBuilder =
new ServerSideAdInsertionStreamRequest.Builder();
requestBuilder.setAssetKey(ASSET_KEY);
requestBuilder.setVideoId(VIDEO_ID);
Assert.assertThrows(IllegalStateException.class, requestBuilder::build);
}
@Test
public void build_assetKeyWithContentSource_throwsIllegalStateException() {
ServerSideAdInsertionStreamRequest.Builder requestBuilder =
new ServerSideAdInsertionStreamRequest.Builder();
requestBuilder.setAssetKey(ASSET_KEY);
requestBuilder.setContentSourceId(CONTENT_SOURCE_ID);
Assert.assertThrows(IllegalStateException.class, requestBuilder::build);
}
@Test
public void build_withoutContentSourceAndVideoIdOrAssetKey_throwsIllegalStateException() {
ServerSideAdInsertionStreamRequest.Builder requestBuilder =
new ServerSideAdInsertionStreamRequest.Builder();
Assert.assertThrows(IllegalStateException.class, requestBuilder::build);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment