This document outlines how to integrate Instream (Pre-roll) video ads into your Android application using ExoPlayer (Media3) and the Google IMA SDK, which is the standard way to handle these ads within the AppLovin MAX ecosystem.
Overview
To provide a premium video experience with native "Skip Ad" buttons and "Ad 1 of 2" indicators, we utilize the Google IMA (Interactive Media Ads) SDK specifically for in-stream video content, while leveraging AppLovin MAX for all other ad formats (Interstitials, App Open, etc.) to maximize global fill rates.
Technical Requirements
- Android 5.0+ (API Level 21+): Required for compatibility with Media3 and the latest AppLovin SDKs.
- AppLovin SDK (Standard Mediation): For handling the primary ad waterfall and mediation logic.
- Google IMA SDK (Direct Integration via Media3): Specifically the media3-exoplayer-ima extension to handle VAST/VMAP parsing and ad rendering overlays.
- Gradle Build System: For managing dependencies and automated builds.
- Network Security:
- HTTPS: All ad tags and media content must be served over secure connections.
- CORS (Cross-Origin Resource Sharing): The server hosting the video files must include CORS headers to allow the IMA SDK to fetch the media.
Ensure your build.gradle includes the necessary Media3 and IMA dependencies:
dependencies {
implementation "androidx.media3:media3-exoplayer:1.2.1"
implementation "androidx.media3:media3-ui:1.2.1"
implementation "androidx.media3:media3-exoplayer-ima:1.2.1"
// AppLovin MAX SDK
implementation "com.applovin:applovin-sdk:<version>"
}
Layout Setup:
Use a PlayerView from Media3 to host both your content and the ad overlay.
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintDimensionRatio="16:9" />Implementation Steps
Initialize the Ads Loader
The ImaAdsLoader manages the communication with Google's IMA servers.
private ImaAdsLoader adsLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
adsLoader = new ImaAdsLoader.Builder(this).build();
}
Configure the Player with Ads Support
You must provide the adsLoader to the MediaSource.Factory so ExoPlayer knows how to insert ads into the timeline.
private void initializePlayer() {
// 1. Create MediaSource Factory with AdsLoader
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(new DefaultDataSource.Factory(this))
.setLocalAdInsertionComponents(unused -> adsLoader, playerView);
// 2. Build ExoPlayer
player = new ExoPlayer.Builder(this)
.setMediaSourceFactory(mediaSourceFactory)
.build();
playerView.setPlayer(player);
adsLoader.setPlayer(player);
}
Create MediaItem with Ad Tag
Define your content URL and the VAST Ad Tag URL provided by your ad representative.
Uri contentUri = Uri.parse("https://your-content-video.mp4");
Uri adTagUri = Uri.parse("https://pubads.g.doubleclick.net/..."); // Your VAST tag
MediaItem mediaItem = new MediaItem.Builder()
.setUri(contentUri)
.setAdsConfiguration(new MediaItem.AdsConfiguration.Builder(adTagUri).build())
.build();
player.setMediaItem(mediaItem);
player.prepare();
player.play();Sample VAST ad tag:
https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&correlator=Handling Ad Events:
You can monitor when an ad is playing to update your UI (e.g., hiding content controls or showing a "Skip" button).
player.addListener(new Player.Listener() { @Override
public void onEvents(Player player, Player.Events events) {
if (player.isPlayingAd()) {
// UI logic for Ad playing (e.g., hide seek bar)
} else {
// UI logic for Content playing
}
}
});Lifecycle Management
It is critical to release the adsLoader and player to prevent memory leaks and ensure ads stop tracking when the activity is destroyed.
@Override
protected void onDestroy() {
super.onDestroy();
if (adsLoader != null) adsLoader.release();
if (player != null) player.release();
}Summary Checklist
- VAST Tag: Ensure you have a valid VAST/VMAP URL from AppLovin/Google.
- IMA Extension: Verify media3-exoplayer-ima is in your dependencies.
- Local Ad Insertion: Always use setLocalAdInsertionComponents when building the player.
- Testing: Use Google's sample VAST tags to verify implementation before using production tags.
InstreamAdAdtivity File from Sample application for reference:
package com.applovin.enterprise.apps.demoapp.ads.max;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.applovin.enterprise.apps.demoapp.R;
import com.applovin.enterprise.apps.demoapp.ui.BaseAdActivity;
import androidx.annotation.OptIn;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.ima.ImaAdsLoader;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.ui.PlayerView;
/**
* An {@link android.app.Activity} used to show AppLovin MAX Instream (Pre-roll) ads using Google IMA SDK.
*/
@UnstableApi
public class InstreamAdActivity extends BaseAdActivity {
private PlayerView playerView;
private ExoPlayer player;
private ImaAdsLoader adsLoader;
private TextView statusTextView;
private Button muteButton;
private Button closeAdButton;
private boolean isMuted = false;
// Sample VAST tag for a pre-roll ad (Google's test tag)
private static final String AD_TAG_URL = "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&correlator=";
// Sample content video
private static final String CONTENT_URL = "https://storage.googleapis.com/gvabox/media/samples/stock.mp4";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_instream_ad);
setTitle("Instream (Pre-roll) Ads");
playerView = findViewById(R.id.player_view);
statusTextView = findViewById(R.id.status_text_view);
Button loadAdButton = findViewById(R.id.load_ad_button);
muteButton = findViewById(R.id.mute_button);
closeAdButton = findViewById(R.id.close_ad_button);
// Initially hide control buttons until player is ready
muteButton.setVisibility(View.GONE);
closeAdButton.setVisibility(View.GONE);
// Create the IMA AdsLoader
adsLoader = new ImaAdsLoader.Builder(this).build();
loadAdButton.setOnClickListener(v -> initializePlayer());
muteButton.setOnClickListener(v -> toggleMute());
closeAdButton.setOnClickListener(v -> skipAd());
}
private void initializePlayer() {
if (player != null) {
player.release();
}
// Set up the MediaSource with the AdsLoader
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(new DefaultDataSource.Factory(this))
.setLocalAdInsertionComponents(unused -> adsLoader, playerView);
player = new ExoPlayer.Builder(this)
.setMediaSourceFactory(mediaSourceFactory)
.build();
playerView.setPlayer(player);
adsLoader.setPlayer(player);
// Use a more comprehensive listener to track ad/content transitions
player.addListener(new Player.Listener() {
@Override
public void onEvents(Player player, Player.Events events) {
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_POSITION_DISCONTINUITY, Player.EVENT_MEDIA_ITEM_TRANSITION)) {
updateUiState();
}
}
});
// Create the MediaItem with the Ad tag
Uri contentUri = Uri.parse(CONTENT_URL);
Uri adTagUri = Uri.parse(AD_TAG_URL);
MediaItem mediaItem = new MediaItem.Builder()
.setUri(contentUri)
.setAdsConfiguration(new MediaItem.AdsConfiguration.Builder(adTagUri).build())
.build();
// Prepare and play
player.setMediaItem(mediaItem);
player.prepare();
player.setPlayWhenReady(true);
// Apply current mute state
player.setVolume(isMuted ? 0f : 1f);
muteButton.setVisibility(View.VISIBLE);
}
private void updateUiState() {
if (player == null) return;
if (player.isPlayingAd()) {
statusTextView.setText("Status: Playing Ad");
closeAdButton.setVisibility(View.VISIBLE);
} else {
statusTextView.setText("Status: Playing Content");
closeAdButton.setVisibility(View.GONE);
}
}
private void toggleMute() {
if (player != null) {
isMuted = !isMuted;
player.setVolume(isMuted ? 0f : 1f);
muteButton.setText(isMuted ? "Unmute" : "Mute");
}
}
private void skipAd() {
if (player != null && player.isPlayingAd()) {
// Try standard SDK skip first
adsLoader.skipAd();
// If the ad is not skippable via SDK, we force the player to seek to the content
// seeking to 0 while in the pre-roll period usually jumps to the main content
if (player.isPlayingAd()) {
player.seekTo(0);
}
}
}
@Override
protected void onStart() {
super.onStart();
if (playerView != null) {
playerView.onResume();
}
}
@Override
protected void onStop() {
super.onStop();
if (playerView != null) {
playerView.onPause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (adsLoader != null) {
adsLoader.release();
}
if (player != null) {
player.release();
}
}
}
Please contact your Freestar Support Team if you need any assistance along the way. We are here to support you!