From cd3bef24b521811b512d5cc404a868ceaadbb615 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 9 Feb 2022 13:33:00 +0000 Subject: [PATCH] Simplify StreamRequest.Builder to an Uri Builder and make it public Right now, the option to build an IMA DAI URI programmatically is still package-private. To simplify the process, we can remove the StreamRequest wrapper and directly provide an URI builder. The same class can provide some package-private helper methods to parse the created URI. #minor-release PiperOrigin-RevId: 427445326 --- .../ImaServerSideAdInsertionMediaSource.java | 42 +- .../ImaServerSideAdInsertionUriBuilder.java | 373 ++++++++++++++ .../ServerSideAdInsertionStreamRequest.java | 477 ------------------ ...maServerSideAdInsertionUriBuilderTest.java | 187 +++++++ ...erverSideAdInsertionStreamRequestTest.java | 148 ------ 5 files changed, 583 insertions(+), 644 deletions(-) create mode 100644 libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilder.java delete mode 100644 libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequest.java create mode 100644 libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilderTest.java delete mode 100644 libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequestTest.java diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 03d99f4afe..bf0f957965 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -323,7 +323,10 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 boolean isLiveStream; + private final String adsId; + private final StreamRequest streamRequest; + private final int loadVideoTimeoutMs; private final StreamPlayer streamPlayer; private final Handler mainHandler; private final ComponentListener componentListener; @@ -354,7 +357,10 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou adPlaybackState = AdPlaybackState.NONE; mainHandler = Util.createHandlerForCurrentLooper(); Uri streamRequestUri = checkNotNull(mediaItem.localConfiguration).uri; - streamRequest = ServerSideAdInsertionStreamRequest.fromUri(streamRequestUri); + isLiveStream = ImaServerSideAdInsertionUriBuilder.isLiveStream(streamRequestUri); + adsId = ImaServerSideAdInsertionUriBuilder.getAdsId(streamRequestUri); + loadVideoTimeoutMs = ImaServerSideAdInsertionUriBuilder.getLoadVideoTimeoutMs(streamRequestUri); + streamRequest = ImaServerSideAdInsertionUriBuilder.createStreamRequest(streamRequestUri); } @Override @@ -371,10 +377,10 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou StreamManagerLoadable streamManagerLoadable = new StreamManagerLoadable( adsLoader, - streamRequest.getStreamRequest(), + streamRequest, streamPlayer, applicationAdErrorListener, - streamRequest.loadVideoTimeoutMs); + loadVideoTimeoutMs); loader.startLoading( streamManagerLoadable, new StreamManagerLoadableCallback(), @@ -483,7 +489,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) { ImmutableMap splitAdPlaybackStates = splitAdPlaybackStateForPeriods(adPlaybackState, contentTimeline); - streamPlayer.setAdPlaybackStates(streamRequest.adsId, splitAdPlaybackStates, contentTimeline); + streamPlayer.setAdPlaybackStates(adsId, splitAdPlaybackStates, contentTimeline); checkNotNull(serverSideAdInsertionMediaSource).setAdPlaybackStates(splitAdPlaybackStates); } } @@ -499,9 +505,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou contentMediaSourceFactory.createMediaSource(MediaItem.fromUri(contentUri)), componentListener); this.serverSideAdInsertionMediaSource = serverSideAdInsertionMediaSource; - if (streamRequest.isLiveStream()) { + if (isLiveStream) { AdPlaybackState liveAdPlaybackState = - new AdPlaybackState(streamRequest.adsId) + new AdPlaybackState(adsId) .withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE) .withIsServerSideInserted(/* adGroupIndex= */ 0, true); mainHandler.post(() -> setAdPlaybackState(liveAdPlaybackState)); @@ -614,7 +620,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou if (!mediaItem.equals(oldPosition.mediaItem) || !mediaItem.equals(newPosition.mediaItem) - || !streamRequest.adsId.equals( + || !adsId.equals( player .getCurrentTimeline() .getPeriodByUid(checkNotNull(newPosition.periodUid), new Timeline.Period()) @@ -648,7 +654,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @Override public void onMetadata(Metadata metadata) { - if (!isCurrentAdPlaying(player, mediaItem, streamRequest.adsId)) { + if (!isCurrentAdPlaying(player, mediaItem, adsId)) { return; } for (int i = 0; i < metadata.length(); i++) { @@ -668,15 +674,14 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @Override public void onPlaybackStateChanged(@Player.State int state) { - if (state == Player.STATE_ENDED - && isCurrentAdPlaying(player, mediaItem, streamRequest.adsId)) { + if (state == Player.STATE_ENDED && isCurrentAdPlaying(player, mediaItem, adsId)) { streamPlayer.onContentCompleted(); } } @Override public void onVolumeChanged(float volume) { - if (!isCurrentAdPlaying(player, mediaItem, streamRequest.adsId)) { + if (!isCurrentAdPlaying(player, mediaItem, adsId)) { return; } int volumePct = (int) Math.floor(volume * 100); @@ -692,15 +697,14 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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)) { + if (!isLiveStream && newAdPlaybackState.equals(AdPlaybackState.NONE)) { newAdPlaybackState = setVodAdGroupPlaceholders( - checkNotNull(streamManager).getCuePoints(), - new AdPlaybackState(streamRequest.adsId)); + checkNotNull(streamManager).getCuePoints(), new AdPlaybackState(adsId)); } break; case LOADED: - if (streamRequest.isLiveStream()) { + if (isLiveStream) { Timeline timeline = player.getCurrentTimeline(); Timeline.Window window = timeline.getWindow(player.getCurrentMediaItemIndex(), new Timeline.Window()); @@ -717,14 +721,14 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou event.getAd(), currentPeriodPosition, newAdPlaybackState.equals(AdPlaybackState.NONE) - ? new AdPlaybackState(streamRequest.adsId) + ? new AdPlaybackState(adsId) : newAdPlaybackState); } else { newAdPlaybackState = setVodAdInPlaceholder(event.getAd(), newAdPlaybackState); } break; case SKIPPED: - if (!streamRequest.isLiveStream()) { + if (!isLiveStream) { newAdPlaybackState = skipAd(event.getAd(), newAdPlaybackState); } break; @@ -742,7 +746,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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; + return !isLiveStream || contentTimeline.getPeriodCount() > 1; } } diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilder.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilder.java new file mode 100644 index 0000000000..c9cf63973e --- /dev/null +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilder.java @@ -0,0 +1,373 @@ +/* + * 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.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; + +import android.net.Uri; +import android.text.TextUtils; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.C.ContentType; +import androidx.media3.common.util.UnstableApi; +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.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +/** + * Builder for URI for IMA DAI streams. The resulting URI can be used to build a {@link + * androidx.media3.common.MediaItem#fromUri(Uri) media item} that can be played by the {@link + * ImaServerSideAdInsertionMediaSource}. + */ +@UnstableApi +public final class ImaServerSideAdInsertionUriBuilder { + + /** The default timeout for loading the video URI, in milliseconds. */ + public static final int DEFAULT_LOAD_VIDEO_TIMEOUT_MS = 10_000; + + private static final String IMA_AUTHORITY = "dai.google.com"; + 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"; + + @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 adTagParameters; + public @ContentType int format; + private int loadVideoTimeoutMs; + + /** Creates a new instance. */ + public ImaServerSideAdInsertionUriBuilder() { + adTagParameters = ImmutableMap.of(); + loadVideoTimeoutMs = DEFAULT_LOAD_VIDEO_TIMEOUT_MS; + format = C.TYPE_OTHER; + } + + /** + * 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 ImaServerSideAdInsertionUriBuilder 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 ImaServerSideAdInsertionUriBuilder 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 ImaServerSideAdInsertionUriBuilder 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 ImaServerSideAdInsertionUriBuilder 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 ImaServerSideAdInsertionUriBuilder 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 ImaServerSideAdInsertionUriBuilder 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 ImaServerSideAdInsertionUriBuilder 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 ImaServerSideAdInsertionUriBuilder setStreamActivityMonitorId( + @Nullable String streamActivityMonitorId) { + this.streamActivityMonitorId = streamActivityMonitorId; + return this; + } + + /** + * Sets the overridable ad tag parameters on the stream request. Supply targeting parameters to your + * stream provides more information. + * + *

You can use the dai-ot and dai-ov parameters for stream variant preference. See Override Stream Variant Parameters + * for more information. + * + * @param adTagParameters A map of extra parameters to pass to the ad server. + * @return This instance, for convenience. + */ + public ImaServerSideAdInsertionUriBuilder setAdTagParameters( + Map 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 ImaServerSideAdInsertionUriBuilder 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 Android + * documentation for more information. + * + * @param contentUrl Deep link to the content's screen. + * @return This instance, for convenience. + */ + public ImaServerSideAdInsertionUriBuilder setContentUrl(@Nullable String contentUrl) { + this.contentUrl = contentUrl; + return this; + } + + /** + * Sets the duration after which resolving the video URI should time out, in milliseconds. + * + *

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 ImaServerSideAdInsertionUriBuilder setLoadVideoTimeoutMs(int loadVideoTimeoutMs) { + this.loadVideoTimeoutMs = loadVideoTimeoutMs; + return this; + } + + /** + * Builds a URI with the builder's current values. + * + * @return The build {@link Uri}. + * @throws IllegalStateException If the builder has missing or invalid inputs. + */ + public Uri build() { + checkState( + (TextUtils.isEmpty(assetKey) + && !TextUtils.isEmpty(contentSourceId) + && !TextUtils.isEmpty(videoId)) + || (!TextUtils.isEmpty(assetKey) + && TextUtils.isEmpty(contentSourceId) + && TextUtils.isEmpty(videoId))); + checkState(format != C.TYPE_OTHER); + @Nullable String adsId = this.adsId; + if (adsId == null) { + adsId = assetKey != null ? assetKey : checkNotNull(videoId); + } + Uri.Builder dataUriBuilder = new Uri.Builder(); + dataUriBuilder.scheme(C.SSAI_SCHEME); + dataUriBuilder.authority(IMA_AUTHORITY); + 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 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(); + } + + /** Returns whether the provided request is for a live stream or false if it is a VOD stream. */ + /* package */ static boolean isLiveStream(Uri uri) { + return !TextUtils.isEmpty(uri.getQueryParameter(ASSET_KEY)); + } + + /** Returns the opaque adsId for this stream. */ + /* package */ static String getAdsId(Uri uri) { + return checkNotNull(uri.getQueryParameter(ADS_ID)); + } + + /** Returns the video load timeout in milliseconds. */ + /* package */ static int getLoadVideoTimeoutMs(Uri uri) { + @Nullable String adsLoaderTimeoutUs = uri.getQueryParameter(LOAD_VIDEO_TIMEOUT_MS); + return TextUtils.isEmpty(adsLoaderTimeoutUs) + ? DEFAULT_LOAD_VIDEO_TIMEOUT_MS + : Integer.parseInt(adsLoaderTimeoutUs); + } + + /** Returns the corresponding {@link StreamRequest}. */ + @SuppressWarnings("nullness") // Required for making nullness test pass for library_with_ima_sdk. + /* package */ static StreamRequest createStreamRequest(Uri uri) { + if (!C.SSAI_SCHEME.equals(uri.getScheme()) || !IMA_AUTHORITY.equals(uri.getAuthority())) { + throw new IllegalArgumentException("Invalid URI scheme or authority."); + } + StreamRequest streamRequest; + // Required params. + @Nullable String assetKey = uri.getQueryParameter(ASSET_KEY); + @Nullable String apiKey = uri.getQueryParameter(API_KEY); + @Nullable String contentSourceId = uri.getQueryParameter(CONTENT_SOURCE_ID); + @Nullable String videoId = uri.getQueryParameter(VIDEO_ID); + if (!TextUtils.isEmpty(assetKey)) { + streamRequest = ImaSdkFactory.getInstance().createLiveStreamRequest(assetKey, apiKey); + } else { + streamRequest = + ImaSdkFactory.getInstance() + .createVodStreamRequest(checkNotNull(contentSourceId), checkNotNull(videoId), apiKey); + } + int format = Integer.parseInt(uri.getQueryParameter(FORMAT)); + if (format == C.TYPE_DASH) { + streamRequest.setFormat(StreamFormat.DASH); + } else if (format == C.TYPE_HLS) { + streamRequest.setFormat(StreamFormat.HLS); + } else { + throw new IllegalArgumentException("Unsupported stream format:" + format); + } + // Optional params. + @Nullable String adTagParametersValue = uri.getQueryParameter(AD_TAG_PARAMETERS); + if (!TextUtils.isEmpty(adTagParametersValue)) { + Map adTagParameters = new HashMap<>(); + Uri adTagParametersUri = Uri.parse(adTagParametersValue); + for (String paramName : adTagParametersUri.getQueryParameterNames()) { + String singleAdTagParameterValue = adTagParametersUri.getQueryParameter(paramName); + if (!TextUtils.isEmpty(singleAdTagParameterValue)) { + adTagParameters.put(paramName, singleAdTagParameterValue); + } + } + streamRequest.setAdTagParameters(adTagParameters); + } + @Nullable String manifestSuffix = uri.getQueryParameter(MANIFEST_SUFFIX); + if (manifestSuffix != null) { + streamRequest.setManifestSuffix(manifestSuffix); + } + @Nullable String contentUrl = uri.getQueryParameter(CONTENT_URL); + if (contentUrl != null) { + streamRequest.setContentUrl(contentUrl); + } + @Nullable String authToken = uri.getQueryParameter(AUTH_TOKEN); + if (authToken != null) { + streamRequest.setAuthToken(authToken); + } + @Nullable String streamActivityMonitorId = uri.getQueryParameter(STREAM_ACTIVITY_MONITOR_ID); + if (streamActivityMonitorId != null) { + streamRequest.setStreamActivityMonitorId(streamActivityMonitorId); + } + return streamRequest; + } +} diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequest.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequest.java deleted file mode 100644 index d8a2345210..0000000000 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequest.java +++ /dev/null @@ -1,477 +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.checkArgument; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Assertions.checkState; - -import android.net.Uri; -import android.text.TextUtils; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.C.ContentType; -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.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 adTagParameters; - public @ContentType 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. Supply targeting parameters to your - * stream provides more information. - * - *

You can use the dai-ot and dai-ov parameters for stream variant preference. See Override Stream Variant Parameters - * 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 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 Android - * documentation 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. - * - *

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 IMA_AUTHORITY = "dai.google.com"; - 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 adTagParameters; - @Nullable public final String manifestSuffix; - @Nullable public final String contentUrl; - @Nullable public final String authToken; - @Nullable public final String streamActivityMonitorId; - public @ContentType 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 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(C.SSAI_SCHEME); - dataUriBuilder.authority(IMA_AUTHORITY); - 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 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 (!C.SSAI_SCHEME.equals(uri.getScheme()) || !IMA_AUTHORITY.equals(uri.getAuthority())) { - throw new IllegalArgumentException("Invalid URI scheme or authority."); - } - 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 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(); - } -} diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilderTest.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilderTest.java new file mode 100644 index 0000000000..e021332f63 --- /dev/null +++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilderTest.java @@ -0,0 +1,187 @@ +/* + * 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 androidx.media3.exoplayer.ima; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.media3.common.C; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.ads.interactivemedia.v3.api.StreamRequest; +import com.google.ads.interactivemedia.v3.api.StreamRequest.StreamFormat; +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 ImaServerSideAdInsertionUriBuilder}. */ +@RunWith(AndroidJUnit4.class) +public final class ImaServerSideAdInsertionUriBuilderTest { + + 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 Map adTagParameters = new HashMap<>(); + + static { + adTagParameters.put("param1", "value1"); + adTagParameters.put("param2", "value2"); + } + + @Test + public void build_live_correctUriParsing() { + ImaServerSideAdInsertionUriBuilder builder = new ImaServerSideAdInsertionUriBuilder(); + 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(C.TYPE_HLS); + builder.setAdTagParameters(adTagParameters); + builder.setLoadVideoTimeoutMs(ADS_LOADER_TIMEOUT_MS); + Uri uri = builder.build(); + + StreamRequest streamRequest = ImaServerSideAdInsertionUriBuilder.createStreamRequest(uri); + assertThat(streamRequest.getAssetKey()).isEqualTo(ASSET_KEY); + assertThat(streamRequest.getApiKey()).isEqualTo(API_KEY); + assertThat(streamRequest.getManifestSuffix()).isEqualTo(MANIFEST_SUFFIX); + assertThat(streamRequest.getContentUrl()).isEqualTo(CONTENT_URL); + assertThat(streamRequest.getAuthToken()).isEqualTo(AUTH_TOKEN); + assertThat(streamRequest.getStreamActivityMonitorId()).isEqualTo(STREAM_ACTIVITY_MONITOR_ID); + assertThat(streamRequest.getFormat()).isEqualTo(StreamFormat.HLS); + assertThat(streamRequest.getAdTagParameters()).isEqualTo(adTagParameters); + + boolean isLive = ImaServerSideAdInsertionUriBuilder.isLiveStream(uri); + assertThat(isLive).isTrue(); + + String adsId = ImaServerSideAdInsertionUriBuilder.getAdsId(uri); + assertThat(adsId).isEqualTo(ADS_ID); + + int loadVideoTimeoutMs = ImaServerSideAdInsertionUriBuilder.getLoadVideoTimeoutMs(uri); + assertThat(loadVideoTimeoutMs).isEqualTo(ADS_LOADER_TIMEOUT_MS); + } + + @Test + public void build_vod_correctUriParsing() { + ImaServerSideAdInsertionUriBuilder builder = new ImaServerSideAdInsertionUriBuilder(); + 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(C.TYPE_DASH); + builder.setAdTagParameters(adTagParameters); + builder.setLoadVideoTimeoutMs(ADS_LOADER_TIMEOUT_MS); + Uri uri = builder.build(); + + StreamRequest streamRequest = ImaServerSideAdInsertionUriBuilder.createStreamRequest(uri); + assertThat(streamRequest.getApiKey()).isEqualTo(API_KEY); + assertThat(streamRequest.getContentSourceId()).isEqualTo(CONTENT_SOURCE_ID); + assertThat(streamRequest.getVideoId()).isEqualTo(VIDEO_ID); + assertThat(streamRequest.getManifestSuffix()).isEqualTo(MANIFEST_SUFFIX); + assertThat(streamRequest.getContentUrl()).isEqualTo(CONTENT_URL); + assertThat(streamRequest.getAuthToken()).isEqualTo(AUTH_TOKEN); + assertThat(streamRequest.getStreamActivityMonitorId()).isEqualTo(STREAM_ACTIVITY_MONITOR_ID); + assertThat(streamRequest.getFormat()).isEqualTo(StreamFormat.DASH); + assertThat(streamRequest.getAdTagParameters()).isEqualTo(adTagParameters); + + boolean isLive = ImaServerSideAdInsertionUriBuilder.isLiveStream(uri); + assertThat(isLive).isFalse(); + + String adsId = ImaServerSideAdInsertionUriBuilder.getAdsId(uri); + assertThat(adsId).isEqualTo(ADS_ID); + + int loadVideoTimeoutMs = ImaServerSideAdInsertionUriBuilder.getLoadVideoTimeoutMs(uri); + assertThat(loadVideoTimeoutMs).isEqualTo(ADS_LOADER_TIMEOUT_MS); + } + + @Test + public void build_vodWithNoAdsId_usesVideoIdAsDefault() { + ImaServerSideAdInsertionUriBuilder builder = new ImaServerSideAdInsertionUriBuilder(); + builder.setContentSourceId(CONTENT_SOURCE_ID); + builder.setVideoId(VIDEO_ID); + builder.setFormat(C.TYPE_DASH); + + Uri streamRequest = builder.build(); + + assertThat(ImaServerSideAdInsertionUriBuilder.getAdsId(streamRequest)).isEqualTo(VIDEO_ID); + assertThat(streamRequest.getQueryParameter("adsId")).isEqualTo(VIDEO_ID); + } + + @Test + public void build_liveWithNoAdsId_usesAssetKeyAsDefault() { + ImaServerSideAdInsertionUriBuilder builder = new ImaServerSideAdInsertionUriBuilder(); + builder.setAssetKey(ASSET_KEY); + builder.setFormat(C.TYPE_DASH); + + Uri streamRequest = builder.build(); + + assertThat(ImaServerSideAdInsertionUriBuilder.getAdsId(streamRequest)).isEqualTo(ASSET_KEY); + assertThat(streamRequest.getQueryParameter("adsId")).isEqualTo(ASSET_KEY); + } + + @Test + public void build_assetKeyWithVideoId_throwsIllegalStateException() { + ImaServerSideAdInsertionUriBuilder requestBuilder = new ImaServerSideAdInsertionUriBuilder(); + requestBuilder.setAssetKey(ASSET_KEY); + requestBuilder.setVideoId(VIDEO_ID); + + Assert.assertThrows(IllegalStateException.class, requestBuilder::build); + } + + @Test + public void build_assetKeyWithContentSource_throwsIllegalStateException() { + ImaServerSideAdInsertionUriBuilder requestBuilder = new ImaServerSideAdInsertionUriBuilder(); + requestBuilder.setAssetKey(ASSET_KEY); + requestBuilder.setContentSourceId(CONTENT_SOURCE_ID); + + Assert.assertThrows(IllegalStateException.class, requestBuilder::build); + } + + @Test + public void build_withoutContentSourceAndVideoIdOrAssetKey_throwsIllegalStateException() { + ImaServerSideAdInsertionUriBuilder requestBuilder = new ImaServerSideAdInsertionUriBuilder(); + + Assert.assertThrows(IllegalStateException.class, requestBuilder::build); + } + + @Test + public void build_withoutLoadVideoTimeoutMs_usesDefaultTimeout() { + Uri uri = + new ImaServerSideAdInsertionUriBuilder() + .setAssetKey(ASSET_KEY) + .setFormat(C.TYPE_DASH) + .build(); + + int loadVideoTimeoutMs = ImaServerSideAdInsertionUriBuilder.getLoadVideoTimeoutMs(uri); + assertThat(loadVideoTimeoutMs) + .isEqualTo(ImaServerSideAdInsertionUriBuilder.DEFAULT_LOAD_VIDEO_TIMEOUT_MS); + } +} diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequestTest.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequestTest.java deleted file mode 100644 index 78b71c2f06..0000000000 --- a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ServerSideAdInsertionStreamRequestTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 androidx.media3.exoplayer.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 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); - } -}