Skip to content

Commit

Permalink
Adds a pod serving sample app.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 643995853
  • Loading branch information
google-ima-devrel-bot authored and IMA Developer Relations committed Jun 17, 2024
1 parent 2f54a08 commit bb8b310
Show file tree
Hide file tree
Showing 24 changed files with 1,146 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
- "BasicExample"
- "ExoPlayerExample"
- "SampleVideoPlayer"
- "PodServingExample"
steps:
- name: Set up JDK 17
uses: actions/setup-java@v1
Expand Down
43 changes: 43 additions & 0 deletions PodServingExample/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
15 changes: 15 additions & 0 deletions PodServingExample/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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 *;
#}
24 changes: 24 additions & 0 deletions PodServingExample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- Required permissions for the video player -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:taskAffinity="">
<activity
android:name=".MyActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit bb8b310

Please sign in to comment.