diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index dffc831..5be144b 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -22,6 +22,7 @@ jobs:
- "BasicExample"
- "ExoPlayerExample"
- "SampleVideoPlayer"
+ - "PodServingExample"
steps:
- name: Set up JDK 17
uses: actions/setup-java@v1
diff --git a/PodServingExample/app/build.gradle b/PodServingExample/app/build.gradle
new file mode 100644
index 0000000..cb4732d
--- /dev/null
+++ b/PodServingExample/app/build.gradle
@@ -0,0 +1,43 @@
+apply plugin: 'com.android.application'
+
+android {
+ namespace 'com.google.ads.interactivemedia.v3.samples.videoplayerapp'
+ compileSdk 34
+
+ // Java 17 required by Gradle 8+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ defaultConfig {
+ applicationId "com.google.ads.interactivemedia.v3.samples.videoplayerapp"
+ minSdkVersion 21
+ targetSdkVersion 34
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ def media3_version = "1.3.1"
+ implementation platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation "androidx.media3:media3-ui:$media3_version"
+ implementation "androidx.media3:media3-exoplayer:$media3_version"
+ implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
+ implementation "androidx.media3:media3-exoplayer-dash:$media3_version"
+ implementation 'androidx.mediarouter:mediarouter:1.7.0'
+ implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.34.0'
+}
diff --git a/PodServingExample/app/proguard-rules.pro b/PodServingExample/app/proguard-rules.pro
new file mode 100644
index 0000000..9d6093f
--- /dev/null
+++ b/PodServingExample/app/proguard-rules.pro
@@ -0,0 +1,15 @@
+# Add project specific ProGuard rules here.
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/PodServingExample/app/src/main/AndroidManifest.xml b/PodServingExample/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..719abb4
--- /dev/null
+++ b/PodServingExample/app/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/samplevideoplayer/SampleVideoPlayer.java b/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/samplevideoplayer/SampleVideoPlayer.java
new file mode 100644
index 0000000..7895308
--- /dev/null
+++ b/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/samplevideoplayer/SampleVideoPlayer.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.ads.interactivemedia.v3.samples.samplevideoplayer;
+
+import static androidx.media3.common.C.CONTENT_TYPE_DASH;
+import static androidx.media3.common.C.CONTENT_TYPE_HLS;
+import static androidx.media3.common.C.CONTENT_TYPE_OTHER;
+import static androidx.media3.common.C.TIME_UNSET;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+import androidx.media3.common.C.ContentType;
+import androidx.media3.common.ForwardingPlayer;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.Metadata;
+import androidx.media3.common.Player;
+import androidx.media3.common.Timeline;
+import androidx.media3.common.util.Util;
+import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.DefaultDataSource;
+import androidx.media3.exoplayer.ExoPlayer;
+import androidx.media3.exoplayer.dash.DashMediaSource;
+import androidx.media3.exoplayer.dash.DefaultDashChunkSource;
+import androidx.media3.exoplayer.hls.HlsMediaSource;
+import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.extractor.metadata.emsg.EventMessage;
+import androidx.media3.extractor.metadata.id3.TextInformationFrame;
+import androidx.media3.ui.PlayerView;
+import com.google.ads.interactivemedia.v3.api.player.VideoStreamPlayer;
+
+/** A video player that plays HLS or DASH streams using ExoPlayer. */
+@SuppressLint("UnsafeOptInUsageError")
+/* @SuppressLint is needed for new media3 APIs. */
+public class SampleVideoPlayer {
+
+ private static final String LOG_TAG = "SampleVideoPlayer";
+
+ /**
+ * Video player callback interface that extends IMA's VideoStreamPlayerCallback by adding the
+ * onSeek() callback to support ad snapback.
+ */
+ public interface SampleVideoPlayerCallback extends VideoStreamPlayer.VideoStreamPlayerCallback {
+ void onSeek(int windowIndex, long positionMs);
+ }
+
+ private final Context context;
+
+ private ExoPlayer player;
+ private final PlayerView playerView;
+ private SampleVideoPlayerCallback playerCallback;
+
+ @ContentType private int currentlyPlayingStreamType = CONTENT_TYPE_OTHER;
+
+ private String streamUrl;
+ private Boolean streamRequested;
+ private boolean canSeek;
+
+ public SampleVideoPlayer(Context context, PlayerView playerView) {
+ this.context = context;
+ this.playerView = playerView;
+ streamRequested = false;
+ canSeek = true;
+ }
+
+ private void initPlayer() {
+ release();
+
+ player = new ExoPlayer.Builder(context).build();
+ playerView.setPlayer(
+ new ForwardingPlayer(player) {
+ @Override
+ public void seekToDefaultPosition() {
+ seekToDefaultPosition(getCurrentMediaItemIndex());
+ }
+
+ @Override
+ public void seekToDefaultPosition(int windowIndex) {
+ seekTo(windowIndex, /* positionMs= */ TIME_UNSET);
+ }
+
+ @Override
+ public void seekTo(long positionMs) {
+ seekTo(getCurrentMediaItemIndex(), positionMs);
+ }
+
+ @Override
+ public void seekTo(int windowIndex, long positionMs) {
+ if (canSeek) {
+ if (playerCallback != null) {
+ playerCallback.onSeek(windowIndex, positionMs);
+ } else {
+ super.seekTo(windowIndex, positionMs);
+ }
+ }
+ }
+ });
+ }
+
+ public void play() {
+ if (streamRequested) {
+ // Stream requested, just resume.
+ player.setPlayWhenReady(true);
+ if (playerCallback != null) {
+ playerCallback.onResume();
+ }
+ return;
+ }
+ initPlayer();
+
+ DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
+
+ // Create the MediaItem to play, specifying the content URI.
+ Uri contentUri = Uri.parse(streamUrl);
+ MediaItem mediaItem = new MediaItem.Builder().setUri(contentUri).build();
+
+ MediaSource mediaSource;
+ currentlyPlayingStreamType = Util.inferContentType(Uri.parse(streamUrl));
+ switch (currentlyPlayingStreamType) {
+ case CONTENT_TYPE_HLS:
+ mediaSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem);
+ break;
+ case CONTENT_TYPE_DASH:
+ mediaSource =
+ new DashMediaSource.Factory(
+ new DefaultDashChunkSource.Factory(dataSourceFactory), dataSourceFactory)
+ .createMediaSource(mediaItem);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown stream type.");
+ }
+
+ player.setMediaSource(mediaSource);
+ player.prepare();
+
+ // Register for ID3 events.
+ player.addListener(
+ new Player.Listener() {
+ @Override
+ public void onMetadata(Metadata metadata) {
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof TextInformationFrame) {
+ TextInformationFrame textFrame = (TextInformationFrame) entry;
+ if ("TXXX".equals(textFrame.id)) {
+ Log.d(LOG_TAG, "Received user text: " + textFrame.values.get(0));
+ if (playerCallback != null) {
+ playerCallback.onUserTextReceived(textFrame.values.get(0));
+ }
+ }
+ } else if (entry instanceof EventMessage) {
+ EventMessage eventMessage = (EventMessage) entry;
+ String eventMessageValue = new String(eventMessage.messageData);
+ Log.d(LOG_TAG, "Received user text: " + eventMessageValue);
+ if (playerCallback != null) {
+ playerCallback.onUserTextReceived(eventMessageValue);
+ }
+ }
+ }
+ }
+ });
+
+ player.setPlayWhenReady(true);
+ streamRequested = true;
+ }
+
+ public void pause() {
+ player.setPlayWhenReady(false);
+ if (playerCallback != null) {
+ playerCallback.onPause();
+ }
+ }
+
+ public void seekTo(long positionMs) {
+ player.seekTo(positionMs);
+ }
+
+ public void seekTo(int windowIndex, long positionMs) {
+ player.seekTo(windowIndex, positionMs);
+ }
+
+ private void release() {
+ if (player != null) {
+ player.release();
+ player = null;
+ streamRequested = false;
+ }
+ }
+
+ public void setStreamUrl(String streamUrl) {
+ this.streamUrl = streamUrl;
+ streamRequested = false; // request new stream on play
+ }
+
+ public void enableControls(boolean doEnable) {
+ if (doEnable) {
+ playerView.showController();
+ } else {
+ playerView.hideController();
+ }
+ canSeek = doEnable;
+ }
+
+ public boolean isStreamRequested() {
+ return streamRequested;
+ }
+
+ // Methods for exposing player information.
+ public void setSampleVideoPlayerCallback(SampleVideoPlayerCallback callback) {
+ playerCallback = callback;
+ }
+
+ /** Returns current offset position of the playhead in milliseconds for DASH and HLS stream. */
+ public long getCurrentPositionMs() {
+ if (player == null) {
+ return 0;
+ }
+ Timeline currentTimeline = player.getCurrentTimeline();
+ if (currentTimeline.isEmpty()) {
+ return player.getCurrentPosition();
+ }
+ Timeline.Window window = new Timeline.Window();
+ player.getCurrentTimeline().getWindow(player.getCurrentMediaItemIndex(), window);
+ if (window.isLive()) {
+ return player.getCurrentPosition() + window.windowStartTimeMs;
+ } else {
+ return player.getCurrentPosition();
+ }
+ }
+
+ public long getDuration() {
+ if (player == null) {
+ return 0;
+ }
+ return player.getDuration();
+ }
+
+ public void setVolume(int percentage) {
+ player.setVolume(percentage);
+ }
+}
diff --git a/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java b/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java
new file mode 100644
index 0000000..ab32d4a
--- /dev/null
+++ b/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.ads.interactivemedia.v3.samples.videoplayerapp;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import com.google.ads.interactivemedia.v3.samples.samplevideoplayer.SampleVideoPlayer;
+
+/** Main Activity that plays media using {@link SampleVideoPlayer}. */
+@SuppressLint("UnsafeOptInUsageError")
+/* @SuppressLint is needed for new media3 APIs. */
+public class MyActivity extends Activity {
+
+ private static final String DEFAULT_STREAM_URL =
+ "https://storage.googleapis.com/interactive-media-ads/media/bbb.m3u8";
+ private static final String APP_LOG_TAG = "ImaDaiExample";
+
+ /** An interface defining how this class emits log messages. */
+ public interface Logger {
+ void log(String logMessage);
+ }
+
+ /** Initializes the logger for displaying events to screen. */
+ private void initializeLogger() {
+ final ScrollView scrollView = findViewById(R.id.logScroll);
+ final TextView textView = findViewById(R.id.logText);
+
+ this.logger =
+ (logMessage -> {
+ Log.i(APP_LOG_TAG, logMessage);
+ if (textView != null) {
+ textView.append(logMessage);
+ }
+ if (scrollView != null) {
+ scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN));
+ }
+ });
+ }
+
+ private Logger logger;
+
+ protected SampleVideoPlayer sampleVideoPlayer;
+ protected ImageButton playButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_my);
+
+ initializeLogger();
+
+ sampleVideoPlayer = new SampleVideoPlayer(MyActivity.this, findViewById(R.id.playerView));
+ playButton = findViewById(R.id.playButton);
+ final SampleAdsWrapper sampleAdsWrapper =
+ new SampleAdsWrapper(this, sampleVideoPlayer, findViewById(R.id.adUiContainer), logger);
+ sampleAdsWrapper.setFallbackUrl(DEFAULT_STREAM_URL);
+
+ // Set up play button listener to play video then hide play button.
+ playButton.setOnClickListener(
+ view -> {
+ sampleVideoPlayer.enableControls(true);
+ sampleAdsWrapper.requestAndPlayAds();
+ playButton.setVisibility(View.GONE);
+ });
+ updateVideoDescriptionVisibility();
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration configuration) {
+ super.onConfigurationChanged(configuration);
+ // Hide the extra content when in landscape so the video is as large as possible.
+ updateVideoDescriptionVisibility();
+ }
+
+ private void updateVideoDescriptionVisibility() {
+ int orientation = getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ findViewById(R.id.descriptionLayout).setVisibility(View.GONE);
+ } else {
+ findViewById(R.id.descriptionLayout).setVisibility(View.VISIBLE);
+ }
+ }
+
+ // Needed to pause/resume app from background.
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (sampleVideoPlayer != null && sampleVideoPlayer.isStreamRequested()) {
+ sampleVideoPlayer.pause();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (sampleVideoPlayer != null && sampleVideoPlayer.isStreamRequested()) {
+ sampleVideoPlayer.play();
+ }
+ }
+}
diff --git a/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java b/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java
new file mode 100644
index 0000000..c2c35aa
--- /dev/null
+++ b/PodServingExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.ads.interactivemedia.v3.samples.videoplayerapp;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
+import com.google.ads.interactivemedia.v3.api.AdEvent;
+import com.google.ads.interactivemedia.v3.api.AdsLoader;
+import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
+import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
+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.StreamRequest.StreamFormat;
+import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
+import com.google.ads.interactivemedia.v3.api.player.VideoStreamPlayer;
+import com.google.ads.interactivemedia.v3.samples.samplevideoplayer.SampleVideoPlayer;
+import com.google.ads.interactivemedia.v3.samples.samplevideoplayer.SampleVideoPlayer.SampleVideoPlayerCallback;
+import com.google.ads.interactivemedia.v3.samples.videoplayerapp.MyActivity.Logger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/** This class implements IMA to add pod ad-serving support to SampleVideoPlayer */
+@SuppressLint("UnsafeOptInUsageError")
+/* @SuppressLint is needed for new media3 APIs. */
+public class SampleAdsWrapper
+ implements AdEvent.AdEventListener, AdErrorEvent.AdErrorListener, AdsLoader.AdsLoadedListener {
+
+ // Set up the pod serving variables.
+ private static final String NETWORK_CODE = "";
+ private static final String CUSTOM_ASSET_KEY = "";
+ private static final String API_KEY = "";
+ private static final String STREAM_URL = "";
+ private static final StreamFormat STREAM_FORMAT = StreamFormat.HLS;
+
+ private enum StreamType {
+ LIVESTREAM,
+ VOD,
+ }
+
+ // Change this enum to make either a live or VOD pod stream request.
+ private static final StreamType CONTENT_STREAM_TYPE = StreamType.LIVESTREAM;
+
+ private final ImaSdkFactory sdkFactory;
+ private final AdsLoader adsLoader;
+ private StreamManager streamManager;
+ private final List playerCallbacks;
+
+ private final SampleVideoPlayer videoPlayer;
+ private final Context context;
+ private final ViewGroup adUiContainer;
+ private final Logger logger;
+ private String fallbackUrl;
+
+ /**
+ * Creates a new SampleAdsWrapper that implements IMA Dynamic Ad Insertion.
+ *
+ * @param context the app's context.
+ * @param videoPlayer underlying ExoPlayer wrapped by the SampleVideoPlayer.
+ * @param adUiContainer ViewGroup that displays the ad UI (ad timer, skip button, adChoices icon).
+ * @param logger Logger to log messages to.
+ */
+ public SampleAdsWrapper(
+ @NonNull Context context,
+ @NonNull SampleVideoPlayer videoPlayer,
+ @NonNull ViewGroup adUiContainer,
+ @NonNull Logger logger) {
+ this.context = context;
+ this.videoPlayer = videoPlayer;
+ this.adUiContainer = adUiContainer;
+ this.logger = logger;
+ playerCallbacks = new ArrayList<>();
+ sdkFactory = ImaSdkFactory.getInstance();
+ adsLoader = createAdsLoader();
+ }
+
+ private AdsLoader createAdsLoader() {
+ ImaSdkSettings settings = sdkFactory.createImaSdkSettings();
+ // Change any settings as necessary here.
+ settings.setDebugMode(true);
+ VideoStreamPlayer videoStreamPlayer = createVideoStreamPlayer();
+ StreamDisplayContainer displayContainer =
+ ImaSdkFactory.createStreamDisplayContainer(adUiContainer, videoStreamPlayer);
+ videoPlayer.setSampleVideoPlayerCallback(
+ new SampleVideoPlayerCallback() {
+ @Override
+ public void onUserTextReceived(@NonNull String userText) {
+ for (VideoStreamPlayer.VideoStreamPlayerCallback callback : playerCallbacks) {
+ callback.onUserTextReceived(userText);
+ }
+ }
+
+ @Override
+ public void onSeek(int windowIndex, long positionMs) {
+ // See if we would seek past an ad, and if so, jump back to it.
+ long newSeekPositionMs = positionMs;
+ if (streamManager != null) {
+ CuePoint prevCuePoint = streamManager.getPreviousCuePointForStreamTimeMs(positionMs);
+ if (prevCuePoint != null && !prevCuePoint.isPlayed()) {
+ newSeekPositionMs = prevCuePoint.getStartTimeMs();
+ }
+ }
+ videoPlayer.seekTo(windowIndex, newSeekPositionMs);
+ }
+
+ @Override
+ public void onContentComplete() {
+ for (VideoStreamPlayer.VideoStreamPlayerCallback callback : playerCallbacks) {
+ callback.onContentComplete();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ for (VideoStreamPlayer.VideoStreamPlayerCallback callback : playerCallbacks) {
+ callback.onPause();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ for (VideoStreamPlayer.VideoStreamPlayerCallback callback : playerCallbacks) {
+ callback.onResume();
+ }
+ }
+
+ @Override
+ public void onVolumeChanged(int percentage) {
+ for (VideoStreamPlayer.VideoStreamPlayerCallback callback : playerCallbacks) {
+ callback.onVolumeChanged(percentage);
+ }
+ }
+ });
+ AdsLoader adsLoader = sdkFactory.createAdsLoader(context, settings, displayContainer);
+ adsLoader.addAdErrorListener(SampleAdsWrapper.this);
+ adsLoader.addAdsLoadedListener(SampleAdsWrapper.this);
+ return adsLoader;
+ }
+
+ public void requestAndPlayAds() {
+ StreamRequest request;
+ switch (CONTENT_STREAM_TYPE) {
+ case LIVESTREAM:
+ // Live pod stream request.
+ request = sdkFactory.createPodStreamRequest(NETWORK_CODE, CUSTOM_ASSET_KEY, API_KEY);
+
+ break;
+ case VOD:
+ // VOD pod stream request.
+ request = sdkFactory.createPodVodStreamRequest(NETWORK_CODE);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected value: " + CONTENT_STREAM_TYPE);
+ }
+ request.setFormat(STREAM_FORMAT);
+ adsLoader.requestStream(request);
+ }
+
+ private VideoStreamPlayer createVideoStreamPlayer() {
+ return new VideoStreamPlayer() {
+ @Override
+ public void loadUrl(@NonNull String url, @NonNull List> subtitles) {
+ // loadUrl() is not called for ad pod streams.
+ // setStreamUrl() and play() are called in onAdsManagerLoaded().
+
+ // Subtitles for VOD pod serving streams are not set here.
+ // Subtitles can be set in the onAdsManagerLoaded() callback using
+ // StreamManager.loadThirdPartyStream(streamUrl, subtitles);
+ }
+
+ @Override
+ public void pause() {
+ // Pause player.
+ videoPlayer.pause();
+ }
+
+ @Override
+ public void resume() {
+ // Resume player.
+ videoPlayer.play();
+ }
+
+ @Override
+ public int getVolume() {
+ // Make the video player play at the current device volume.
+ return 100;
+ }
+
+ @Override
+ public void addCallback(@NonNull VideoStreamPlayerCallback videoStreamPlayerCallback) {
+ playerCallbacks.add(videoStreamPlayerCallback);
+ }
+
+ @Override
+ public void removeCallback(@NonNull VideoStreamPlayerCallback videoStreamPlayerCallback) {
+ playerCallbacks.remove(videoStreamPlayerCallback);
+ }
+
+ @Override
+ public void onAdBreakStarted() {
+ // Disable player controls.
+ videoPlayer.enableControls(false);
+ logger.log("Ad Break Started\n");
+ }
+
+ @Override
+ public void onAdBreakEnded() {
+ // Re-enable player controls.
+ if (videoPlayer != null) {
+ videoPlayer.enableControls(true);
+ }
+ logger.log("Ad Break Ended\n");
+ }
+
+ @Override
+ public void onAdPeriodStarted() {
+ logger.log("Ad Period Started\n");
+ }
+
+ @Override
+ public void onAdPeriodEnded() {
+ logger.log("Ad Period Ended\n");
+ }
+
+ @Override
+ public void seek(@NonNull long timeMs) {
+ // An ad was skipped. Skip to the content time.
+ videoPlayer.seekTo(timeMs);
+ logger.log("seek\n");
+ }
+
+ @NonNull
+ @Override
+ public VideoProgressUpdate getContentProgress() {
+ return new VideoProgressUpdate(
+ videoPlayer.getCurrentPositionMs(), videoPlayer.getDuration());
+ }
+ };
+ }
+
+ /** AdErrorListener implementation */
+ @Override
+ public void onAdError(@NonNull AdErrorEvent event) {
+ logger.log(String.format("Error: %s\n", event.getError().getMessage()));
+ // play fallback URL.
+ logger.log("Playing fallback Url\n");
+ videoPlayer.setStreamUrl(fallbackUrl);
+ videoPlayer.enableControls(true);
+ videoPlayer.play();
+ }
+
+ /** AdEventListener implementation */
+ @Override
+ public void onAdEvent(@NonNull AdEvent event) {
+ switch (event.getType()) {
+ case AD_PROGRESS:
+ // Do nothing or else log will be filled by these messages.
+ break;
+ default:
+ logger.log(String.format("Event: %s\n", event.getType()));
+ break;
+ }
+ }
+
+ /** AdsLoadedListener implementation */
+ @Override
+ public void onAdsManagerLoaded(@NonNull AdsManagerLoadedEvent event) {
+ streamManager = event.getStreamManager();
+ streamManager.addAdErrorListener(this);
+ streamManager.addAdEventListener(this);
+
+ AdsRenderingSettings adsRenderingSettings = sdkFactory.createAdsRenderingSettings();
+ // Add any ads rendering settings here.
+ // This init() only loads the UI rendering settings locally.
+ streamManager.init(adsRenderingSettings);
+
+ // To enable ad pod streams
+ String streamID = streamManager.getStreamId();
+ switch (CONTENT_STREAM_TYPE) {
+ case LIVESTREAM:
+ // Play the live pod stream.
+ String streamUrl = STREAM_URL.replace("[[STREAMID]]", streamID);
+ videoPlayer.setStreamUrl(streamUrl);
+ videoPlayer.play();
+ // The SDK doesn't call the loadUrl() function for livestream.
+ break;
+ case VOD:
+ // TODO 'vtpInterface' is a place holder for your own video technology partner
+ // (VTP) API calls. To use the VOD_POD part of this sample, additional code changes
+ // are required to integrate your VTP. The below lines are sample code for this setup.
+ // String streamUrl = vtpInterface.requestStreamURL(streamID);
+ // Pass any subtitles returned by your VTP in this step as well.
+ // streamManager.loadThirdPartyStream(streamUrl, subtitles);
+ // Once the ad pod metadata loading is complete, the SDK will call the loadUrl() function.
+ logger.log("Additional setup is needed to make VOD pod stream requests with this app.");
+ break;
+ }
+ }
+
+ /** Sets fallback URL in case ads stream fails. */
+ void setFallbackUrl(String url) {
+ fallbackUrl = url;
+ }
+}
diff --git a/PodServingExample/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png b/PodServingExample/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png
new file mode 100644
index 0000000..157f6b2
Binary files /dev/null and b/PodServingExample/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png differ
diff --git a/PodServingExample/app/src/main/res/drawable-hdpi/ic_launcher.png b/PodServingExample/app/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..72ebaad
Binary files /dev/null and b/PodServingExample/app/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/PodServingExample/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png b/PodServingExample/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png
new file mode 100644
index 0000000..e4fb0dd
Binary files /dev/null and b/PodServingExample/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png differ
diff --git a/PodServingExample/app/src/main/res/drawable-mdpi/ic_launcher.png b/PodServingExample/app/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..5ad8cb4
Binary files /dev/null and b/PodServingExample/app/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/PodServingExample/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png b/PodServingExample/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png
new file mode 100644
index 0000000..47d09cb
Binary files /dev/null and b/PodServingExample/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png differ
diff --git a/PodServingExample/app/src/main/res/drawable-xhdpi/ic_launcher.png b/PodServingExample/app/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..a734148
Binary files /dev/null and b/PodServingExample/app/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/PodServingExample/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/PodServingExample/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..585e71d
Binary files /dev/null and b/PodServingExample/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/PodServingExample/app/src/main/res/layout/activity_my.xml b/PodServingExample/app/src/main/res/layout/activity_my.xml
new file mode 100644
index 0000000..356ca91
--- /dev/null
+++ b/PodServingExample/app/src/main/res/layout/activity_my.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PodServingExample/app/src/main/res/values-w820dp/dimens.xml b/PodServingExample/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/PodServingExample/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/PodServingExample/app/src/main/res/values/dimens.xml b/PodServingExample/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..186f1bb
--- /dev/null
+++ b/PodServingExample/app/src/main/res/values/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 16dp
+ 16dp
+ 16sp
+
diff --git a/PodServingExample/app/src/main/res/values/strings.xml b/PodServingExample/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..78e3298
--- /dev/null
+++ b/PodServingExample/app/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+
+ IMA Sample Pod Serving app
+ Settings
+ Sample Video Stream
+ #000000
+
+
diff --git a/PodServingExample/build.gradle b/PodServingExample/build.gradle
new file mode 100644
index 0000000..ca069a7
--- /dev/null
+++ b/PodServingExample/build.gradle
@@ -0,0 +1,14 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.4.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
diff --git a/PodServingExample/gradle.properties b/PodServingExample/gradle.properties
new file mode 100644
index 0000000..e3dfef4
--- /dev/null
+++ b/PodServingExample/gradle.properties
@@ -0,0 +1,5 @@
+android.defaults.buildfeatures.buildconfig=true
+android.enableJetifier=true
+android.nonFinalResIds=false
+android.nonTransitiveRClass=false
+android.useAndroidX=true
\ No newline at end of file
diff --git a/PodServingExample/gradle/wrapper/gradle-wrapper.properties b/PodServingExample/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..17655d0
--- /dev/null
+++ b/PodServingExample/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/PodServingExample/gradlew b/PodServingExample/gradlew
new file mode 100755
index 0000000..86d2bbc
--- /dev/null
+++ b/PodServingExample/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted through cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/PodServingExample/gradlew.bat b/PodServingExample/gradlew.bat
new file mode 100644
index 0000000..1f4d661
--- /dev/null
+++ b/PodServingExample/gradlew.bat
@@ -0,0 +1,69 @@
+
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+goto fail
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+if exist "%JAVA_EXE%" goto execute
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+goto fail
+:execute
+@rem Setup the command line
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+:omega
diff --git a/PodServingExample/settings.gradle b/PodServingExample/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/PodServingExample/settings.gradle
@@ -0,0 +1 @@
+include ':app'