diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5be144b..332432e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,6 +20,7 @@ jobs: regex: - "AdvancedExample" - "BasicExample" + - "CloudVideoStitcherExample" - "ExoPlayerExample" - "SampleVideoPlayer" - "PodServingExample" diff --git a/AdvancedExample/app/build.gradle b/AdvancedExample/app/build.gradle index 40d0eeb..96a0c99 100644 --- a/AdvancedExample/app/build.gradle +++ b/AdvancedExample/app/build.gradle @@ -12,7 +12,7 @@ android { defaultConfig { applicationId "com.google.ads.interactivemedia.v3.samples.videoplayerapp" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -34,7 +34,7 @@ repositories { dependencies { def media3_version = "1.3.1" implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.media3:media3-ui:$media3_version" implementation "androidx.media3:media3-exoplayer:$media3_version" @@ -42,5 +42,5 @@ dependencies { implementation "androidx.media3:media3-exoplayer-dash:$media3_version" implementation 'androidx.mediarouter:mediarouter:1.7.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' - implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.33.0' + implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.34.0' } diff --git a/AdvancedExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java b/AdvancedExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java index 6529b47..f414a0c 100644 --- a/AdvancedExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java +++ b/AdvancedExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java @@ -19,6 +19,7 @@ import android.content.Context; import android.util.Log; 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; @@ -47,15 +48,14 @@ public interface Logger { void log(String logMessage); } - private ImaSdkFactory sdkFactory; + private final ImaSdkFactory sdkFactory; private AdsLoader adsLoader; private StreamManager streamManager; - private StreamDisplayContainer displayContainer; - private List playerCallbacks; + private final List playerCallbacks; private SampleVideoPlayer videoPlayer; - private Context context; - private ViewGroup adUiContainer; + private final Context context; + private final ViewGroup adUiContainer; private long bookMarkContentTimeMs; // Bookmarked content time, in milliseconds. private long snapBackTimeMs; // Stream time to snap back to, in milliseconds. @@ -84,11 +84,12 @@ private void createAdsLoader() { // Change any settings as necessary here. settings.setPlayerType(PLAYER_TYPE); VideoStreamPlayer videoStreamPlayer = createVideoStreamPlayer(); - displayContainer = ImaSdkFactory.createStreamDisplayContainer(adUiContainer, videoStreamPlayer); + StreamDisplayContainer displayContainer = + ImaSdkFactory.createStreamDisplayContainer(adUiContainer, videoStreamPlayer); videoPlayer.setSampleVideoPlayerCallback( new SampleVideoPlayer.SampleVideoPlayerCallback() { @Override - public void onUserTextReceived(String userText) { + public void onUserTextReceived(@NonNull String userText) { for (VideoStreamPlayer.VideoStreamPlayerCallback callback : playerCallbacks) { callback.onUserTextReceived(userText); } @@ -184,7 +185,7 @@ private StreamRequest buildStreamRequest(VideoListFragment.VideoListItem videoLi private VideoStreamPlayer createVideoStreamPlayer() { return new VideoStreamPlayer() { @Override - public void loadUrl(String url, List> subtitles) { + public void loadUrl(@NonNull String url, @NonNull List> subtitles) { videoPlayer.setStreamUrl(url); videoPlayer.play(); @@ -214,12 +215,12 @@ public int getVolume() { } @Override - public void addCallback(VideoStreamPlayerCallback videoStreamPlayerCallback) { + public void addCallback(@NonNull VideoStreamPlayerCallback videoStreamPlayerCallback) { playerCallbacks.add(videoStreamPlayerCallback); } @Override - public void removeCallback(VideoStreamPlayerCallback videoStreamPlayerCallback) { + public void removeCallback(@NonNull VideoStreamPlayerCallback videoStreamPlayerCallback) { playerCallbacks.remove(videoStreamPlayerCallback); } @@ -263,6 +264,7 @@ public void seek(long timeMs) { videoPlayer.seekTo(timeMs); } + @NonNull @Override public VideoProgressUpdate getContentProgress() { if (videoPlayer == null) { diff --git a/AdvancedExample/gradle/wrapper/gradle-wrapper.properties b/AdvancedExample/gradle/wrapper/gradle-wrapper.properties index 7a2386e..5d6560a 100644 --- a/AdvancedExample/gradle/wrapper/gradle-wrapper.properties +++ b/AdvancedExample/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,3 @@ -#Mon Jan 14 17:57:16 PST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/BasicExample/app/build.gradle b/BasicExample/app/build.gradle index dcc3e69..de2af98 100644 --- a/BasicExample/app/build.gradle +++ b/BasicExample/app/build.gradle @@ -12,7 +12,7 @@ android { defaultConfig { applicationId "com.google.ads.interactivemedia.v3.samples.videoplayerapp" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -34,11 +34,11 @@ repositories { dependencies { def media3_version = "1.3.1" implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) - implementation 'androidx.appcompat:appcompat:1.6.1' + 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.33.0' + implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.34.0' } diff --git a/BasicExample/app/src/main/res/layout/activity_my.xml b/BasicExample/app/src/main/res/layout/activity_my.xml index 4830a7e..6ddd50e 100644 --- a/BasicExample/app/src/main/res/layout/activity_my.xml +++ b/BasicExample/app/src/main/res/layout/activity_my.xml @@ -8,8 +8,7 @@ tools:ignore="MergeRootFrame"> - + + + + + + + + + + + + + + + + + diff --git a/CloudVideoStitcherExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/samplevideoplayer/SampleVideoPlayer.java b/CloudVideoStitcherExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/samplevideoplayer/SampleVideoPlayer.java new file mode 100644 index 0000000..7895308 --- /dev/null +++ b/CloudVideoStitcherExample/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/CloudVideoStitcherExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java b/CloudVideoStitcherExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java new file mode 100644 index 0000000..ab32d4a --- /dev/null +++ b/CloudVideoStitcherExample/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/CloudVideoStitcherExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java b/CloudVideoStitcherExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java new file mode 100644 index 0000000..bbb46fe --- /dev/null +++ b/CloudVideoStitcherExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/SampleAdsWrapper.java @@ -0,0 +1,310 @@ +/* + * 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 Cloud Video Stitcher 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 Cloud Video Stitcher variables. + private static final String NETWORK_CODE = ""; + private static final String LOCATION = ""; + private static final String PROJECT_NUMBER = ""; + private static final String OAUTH_TOKEN = ""; + private static final StreamFormat STREAM_FORMAT = StreamFormat.HLS; + + // Livestream variables. + private static final String ASSET_KEY = ""; + private static final String LIVE_CONFIG_ID = ""; + + // VOD variables. + private static final String VOD_CONFIG_ID = ""; + + private enum StreamType { + LIVESTREAM, + VOD, + } + + // Change this enum to make either a live or VOD 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: + // Livestream Cloud Video Stitcher stream request. + request = + sdkFactory.createVideoStitcherLiveStreamRequest( + NETWORK_CODE, ASSET_KEY, LIVE_CONFIG_ID, LOCATION, PROJECT_NUMBER, OAUTH_TOKEN); + break; + case VOD: + // VOD Cloud Video Stitcher stream request. + request = + sdkFactory.createVideoStitcherVodStreamRequest( + NETWORK_CODE, LOCATION, PROJECT_NUMBER, OAUTH_TOKEN, VOD_CONFIG_ID); + 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 streamUrl, @NonNull List> subtitles) { + videoPlayer.setStreamUrl(streamUrl); + videoPlayer.play(); + } + + @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); + } + + /** Sets fallback URL in case ads stream fails. */ + void setFallbackUrl(String url) { + fallbackUrl = url; + } +} diff --git a/CloudVideoStitcherExample/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png b/CloudVideoStitcherExample/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/CloudVideoStitcherExample/app/src/main/res/drawable-hdpi/ic_action_play_over_video.png differ diff --git a/CloudVideoStitcherExample/app/src/main/res/drawable-hdpi/ic_launcher.png b/CloudVideoStitcherExample/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..72ebaad Binary files /dev/null and b/CloudVideoStitcherExample/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/CloudVideoStitcherExample/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png b/CloudVideoStitcherExample/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/CloudVideoStitcherExample/app/src/main/res/drawable-mdpi/ic_action_play_over_video.png differ diff --git a/CloudVideoStitcherExample/app/src/main/res/drawable-mdpi/ic_launcher.png b/CloudVideoStitcherExample/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..5ad8cb4 Binary files /dev/null and b/CloudVideoStitcherExample/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/CloudVideoStitcherExample/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png b/CloudVideoStitcherExample/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/CloudVideoStitcherExample/app/src/main/res/drawable-xhdpi/ic_action_play_over_video.png differ diff --git a/CloudVideoStitcherExample/app/src/main/res/drawable-xhdpi/ic_launcher.png b/CloudVideoStitcherExample/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a734148 Binary files /dev/null and b/CloudVideoStitcherExample/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/CloudVideoStitcherExample/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/CloudVideoStitcherExample/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..585e71d Binary files /dev/null and b/CloudVideoStitcherExample/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/CloudVideoStitcherExample/app/src/main/res/layout/activity_my.xml b/CloudVideoStitcherExample/app/src/main/res/layout/activity_my.xml new file mode 100644 index 0000000..356ca91 --- /dev/null +++ b/CloudVideoStitcherExample/app/src/main/res/layout/activity_my.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/CloudVideoStitcherExample/app/src/main/res/values-w820dp/dimens.xml b/CloudVideoStitcherExample/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..63fc816 --- /dev/null +++ b/CloudVideoStitcherExample/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/CloudVideoStitcherExample/app/src/main/res/values/dimens.xml b/CloudVideoStitcherExample/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..186f1bb --- /dev/null +++ b/CloudVideoStitcherExample/app/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + 16dp + 16dp + 16sp + diff --git a/CloudVideoStitcherExample/app/src/main/res/values/strings.xml b/CloudVideoStitcherExample/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..de9d699 --- /dev/null +++ b/CloudVideoStitcherExample/app/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + + IMA Sample Cloud Video Stitcher app + Settings + Sample Video Stream + #000000 + + diff --git a/CloudVideoStitcherExample/build.gradle b/CloudVideoStitcherExample/build.gradle new file mode 100644 index 0000000..72e8e2f --- /dev/null +++ b/CloudVideoStitcherExample/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.3.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} diff --git a/CloudVideoStitcherExample/gradle.properties b/CloudVideoStitcherExample/gradle.properties new file mode 100644 index 0000000..5465fec --- /dev/null +++ b/CloudVideoStitcherExample/gradle.properties @@ -0,0 +1,2 @@ +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file diff --git a/CloudVideoStitcherExample/gradle/wrapper/gradle-wrapper.properties b/CloudVideoStitcherExample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..17655d0 --- /dev/null +++ b/CloudVideoStitcherExample/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/CloudVideoStitcherExample/gradlew b/CloudVideoStitcherExample/gradlew new file mode 100755 index 0000000..86d2bbc --- /dev/null +++ b/CloudVideoStitcherExample/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/CloudVideoStitcherExample/gradlew.bat b/CloudVideoStitcherExample/gradlew.bat new file mode 100644 index 0000000..8fca95a --- /dev/null +++ b/CloudVideoStitcherExample/gradlew.bat @@ -0,0 +1,90 @@ +@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 + +@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= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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 init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo 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 init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +: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 %CMD_LINE_ARGS% + +: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/CloudVideoStitcherExample/settings.gradle b/CloudVideoStitcherExample/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/CloudVideoStitcherExample/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/ExoPlayerExample/app/build.gradle b/ExoPlayerExample/app/build.gradle index 147258c..83b0a96 100644 --- a/ExoPlayerExample/app/build.gradle +++ b/ExoPlayerExample/app/build.gradle @@ -12,7 +12,7 @@ android { defaultConfig { applicationId "com.google.ads.interactivemedia.v3.samples.videoplayerapp" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 34 multiDexEnabled true versionCode 1 @@ -35,7 +35,7 @@ dependencies { def media3_version = "1.3.1" implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.appcompat:appcompat:1.6.1' + 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" diff --git a/ExoPlayerExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java b/ExoPlayerExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java index 8ef709c..0f49c45 100644 --- a/ExoPlayerExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java +++ b/ExoPlayerExample/app/src/main/java/com/google/ads/interactivemedia/v3/samples/videoplayerapp/MyActivity.java @@ -9,6 +9,7 @@ import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media3.common.MediaItem; import androidx.media3.common.util.Util; @@ -100,7 +101,7 @@ public void onStop() { } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { // Attempts to save the AdsLoader state to handle app backgrounding. if (adsLoaderState != null) { outState.putBundle(KEY_ADS_LOADER_STATE, adsLoaderState.toBundle()); diff --git a/ExoPlayerExample/gradle/wrapper/gradle-wrapper.properties b/ExoPlayerExample/gradle/wrapper/gradle-wrapper.properties index 303355b..5d6560a 100644 --- a/ExoPlayerExample/gradle/wrapper/gradle-wrapper.properties +++ b/ExoPlayerExample/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,3 @@ -#Wed Dec 12 10:57:27 PST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/PodServingExample/build.gradle b/PodServingExample/build.gradle index ca069a7..72e8e2f 100644 --- a/PodServingExample/build.gradle +++ b/PodServingExample/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.4.0' + classpath 'com.android.tools.build:gradle:8.3.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/SampleVideoPlayer/app/build.gradle b/SampleVideoPlayer/app/build.gradle index cff8123..b482d22 100644 --- a/SampleVideoPlayer/app/build.gradle +++ b/SampleVideoPlayer/app/build.gradle @@ -12,7 +12,7 @@ android { defaultConfig { applicationId "com.google.ads.interactivemedia.v3.samples.videoplayerapp" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -33,8 +33,9 @@ repositories { dependencies { def media3_version = "1.3.1" + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.appcompat:appcompat:1.6.1' + 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" diff --git a/SampleVideoPlayer/app/src/main/res/layout/activity_my.xml b/SampleVideoPlayer/app/src/main/res/layout/activity_my.xml index 3eb0359..bb72afb 100644 --- a/SampleVideoPlayer/app/src/main/res/layout/activity_my.xml +++ b/SampleVideoPlayer/app/src/main/res/layout/activity_my.xml @@ -8,8 +8,7 @@ tools:ignore="MergeRootFrame"> - +