Video player implementation (App: Android)
Hidden pageThis page is currently hidden in Criteo's documentation. This means it won't appear in the navigation menu and is only accessible with the link. To return to this page in the future, please make sure to bookmark it.
Android VS iOSThis page only covers the video player implementation for Android. Please refer to this page for the iOS implementation. We will offer code examples for both platforms, and any platform-specific details will be clearly highlighted where applicable.
Commerce Video for Apps is in BetaThe documentation and features are likely to change, and the product might contain bugs.
Please report any issues and check back for updates.
Introduction
This document aims at providing a detailed guideline about implementing On-Site Commerce Video ads on mobile platforms, Android and iOS.
This page focuses on mobile implementation. For a broader context, please visit the video player implementation guide for desktop.
Android OM SDK DocumentationFor more information on the Android OM SDK, you can also visit this page from IAB.
We use OM SDK provided by IAB, for their library they indicate
minSdkVersion="16"andtargetSdkVersion="30". Any Android application using the SDK must be compatible with devices running at least Android 4.1 (API level 16), and the app is optimized for Android 11 (API level 30).
Video Player Implementation guide
When it comes to building your video ad implementation, there are a few high-level steps to consider:
- Requesting and parsing the VAST tag,
- Creating the HTML structure of your player to be injected into the DOM,
- Firing beacons at the proper time during video playback,
- Incorporating additional verification scripts (OMID).
Criteo supports VAST 4.2 (the IAB specs are available here).
Step 1: requesting and Parsing the VAST Tag
To obtain the data for your video player, start by fetching the VAST information from the Criteo API Ad Response.
Here is an example of how to request the VAST (if required) and parse it:
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse('VAST TAG URL');
doc.getDocumentElement().normalize();
dBuilderqueries VAST URL internally, since this is a network query it should not be executed in the main UI thread (ex. how to execute network requests on Android)
Once you have the parsed VAST tag, you can extract the necessary information outlined in this document, including:
- The link for the video ad,
- VAST XML tracking beacons,
- OMID Verification script and
VerificationParameters, - Closed captioning files.
Step 2: creating the video player
Our video player is built around native mobile video players (for Android we use ExoPlayer), they offer methods like play(), pause(), along with properties such as: volume, duration and currentPosition.
These properties help us interact with the video player to mute or unmute and track playback progress.
ExoPlayer for AndroidLearn more about ExoPlayer for Android on this page.
To manage these functionalities, we incorporate additional control elements for the Play/Pause and Mute/Unmute and CC (close captions) buttons.
We add click listeners to these buttons to handle play, pause, and volume controls.
Additionally, be aware that local regulations may require a callout or pop-up for mandatory notices.
We need to set up a UI layout for the player. It just contains the video player for the ad and a mute button.
<androidx.media3.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/muteTextView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="Mute" />
</LinearLayout>
</androidx.media3.ui.PlayerView>With the layout in place, the next step is to instantiate the component in order to:
- Get handles on the UI elements,
- Set click listeners on them,
- Query and parse VAST,
- Prepare the player and launch the ad.
public class VideoAdNativeActivity extends Activity implements Player.Listener, View.OnClickListener {
private static final int PLAYER_UNMUTE = 1;
private static final int PLAYER_MUTE = 0;
private PlayerView playerView;
private ExoPlayer player;
private TextView muteTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.video_ad_native_detail);
playerView = findViewById(R.id.videoView);
muteTextView = findViewById(R.id.muteTextView);
playerView.requestFocus();
playerView.setOnClickListener(this);
muteTextView.setOnClickListener(this);
// Query and parse VAST
...
// Instantiate the video ad player with a media item from the VAST
initializePlayer(videoUrl);
}
private void initializePlayer(String videoUrl) {
// Create an ExoPlayer and set it as the player for content and ads.
player = new ExoPlayer.Builder(this)
.build();
// Create the MediaItem to play
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(Uri.parse(videoUrl))
.build();
player.setMediaItem(mediaItem);
// Add listener on player events
player.addListener(this);
player.setVolume(PLAYER_VOLUME);
playerView.setPlayer(player);
// Disable default player controls (default controls allow seeking)
playerView.setUseController(false);
player.setPlayWhenReady(true);
player.prepare();
}
}
Step 3: setting up OMID ad session
Clients must support OMID to enable their player to run additional scripts, hence allowing Criteo to track video viewability. This allows Criteo to send its own verification script within the VAST XML to track video viewability.
About OMIDThe Open Measurement Interface Definition (OMID) is a standardized technology provided by IAB to measure and verify the viewability of digital ads, particularly video ads, across various platforms and devices.
OMID provides a consistent framework for measuring viewability metrics, such as viewable impressions and viewable video plays, in compliance with industry standards. Implementing OMID offers accurate insights into ad performance and user engagement, and is also used to track video viewability the same way across all retailers, ensuring coherence across networks.
OM SDK in this documentation refers to the OM SDK for mobile platforms (Android/iOS).
Section 3.16 of the VAST specification outlines an optional AdVerifications element where these verifications reside. Each Verification element nested inside AdVerifications details the resources needed for verification.
The Open-measurement SDKTo handle these resources, we pass them to the Open-Measurement SDK provided by IAB. The official OM SDK implementation guide for Android can be found here. The documentation provides full code snippets for each step to integrate viewability.
At a high level, the process goes as follows:
- Load the OMID service script,
- Create an ad session configuration and the ad session itself,
- Register the player showing the ad and start the ad session,
- Trigger
adEvents&mediaEventsevents to OM SDK when Video & Ad related events occur.
The IAB implementation guide is not complete enough to cover all the Criteo video player specifications (support of buffering events, only emit tracking events once, play/pause events are not notifying OM SDK, etc.). For that reason, it is highly recommended to also verify your integration following the IAB documentation.
The Criteo verification script comes from the VAST XML file.
Through the OM SDK, several events will be emitted by Criteo OMID Verification Script to Criteo servers:
| Level | Beacon name | Description |
|---|---|---|
OMID (VerificationParameters) | omidTrackView | Indicates that the OMID Ad session was properly initialized.. |
OMID (VerificationParameters) | start | Indicates that the video just started, and the video is displayed 100% in the viewport. |
OMID (VerificationParameters) | firstQuartile | Indicates that the video just reached the 25% quartile, and the video is displayed 100% in the viewport. |
OMID (VerificationParameters) | midpoint | Indicates that the video just reached the 50% quartile, and the video is displayed 100% in the viewport. |
OMID (VerificationParameters) | thirdQuartile | Indicates that the video just reached the 75% quartile, and the video is displayed 100% in the viewport. |
OMID (VerificationParameters) | complete | Indicates that the video just completed, and the video is displayed 100% in the viewport. |
OMID (VerificationParameters) | twoSecondsFiftyPercentView | Indicates that the video was playing for 2 consecutive seconds, and the video is at least displayed 50% in the viewport. Note that, buffering the video or pausing, will reset the 2 seconds counter. |
| VAST XML - OMID | <Tracking event="verificationNotExecuted"> | Indicates that the verification script failed to initialize. |
The 100% in the viewport display is an industry standard to track playback events such as quartiles. The two consecutive seconds and 50% display in the viewport is an MRC standard.
1. Load the OMID service script
The provided IAB OM SDK is composed of a script (omweb-v1.js) and a mobile library that need to be included for the creative to support viewability.
The script can either be self-hosted or pulled from Criteo’s CDN, where we host copies. The file can be found on this page.Omweb-v1.js is the OMID JS library for the web context.
2. Create an ad session configuration and the ad session itself
Before the ad session initialization, the OM SDK needs to be activated. The ad session also requires specifying some parameters related to the Criteo integration.
OMID
VerificationScript,VerificationParametersandVendorKeyare provided inside the VAST.
public AdSession getNativeAdSession(Context context) {
Omid.activate(context.getApplicationContext());
AdSessionConfiguration adSessionConfiguration =
AdSessionConfiguration.createAdSessionConfiguration(
CreativeType.VIDEO,
ImpressionType.VIEWABLE,
Owner.NATIVE,
Owner.NATIVE, false);
Partner partner = Partner.createPartner("criteo", "<version name>");
final String omidJs = getOmidJs(context);
VerificationScriptResource verificationScripts =
VerificationScriptResource.createVerificationScriptResourceWithParameters(
vendorKey,
omidVerificationScript,
omidVerificationParameters);
AdSessionContext adSessionContext = AdSessionContext.createNativeAdSessionContext(
partner,
omidJs,
Collections.singletonList(verificationScripts),
null,
null);
AdSession adSession = AdSession.createAdSession(adSessionConfiguration, adSessionContext);
return adSession;
}
public static String getOmidJs(Context context) {
Resources res = context.getResources();
try (InputStream inputStream = res.openRawResource(R.raw.omsdk_v1)) {
byte[] b = new byte[inputStream.available()];
final int bytesRead = inputStream.read(b);
return new String(b, 0, bytesRead, "UTF-8");
} catch (IOException e) {
throw new UnsupportedOperationException("OM SDK JS library resource not found", e);
}
}
3. Register the player showing the ad and start the ad session
When the ad session is started, the Criteo verification script is automatically loaded. At this point, the Criteo verification script will start observing any AdEvents & MediaEvents that are triggered toward the OM SDK.
In the case that OMID session preparation fails, it is important to call the
verificationNotExecutedVAST tracker (if it’s present in the VAST).
public class VideoAdNativeActivity extends Activity implements Player.Listener, View.OnClickListener {
[...]
private MediaEvents mediaEvents;
private AdEvents adEvents;
@Override
public void onCreate(Bundle savedInstanceState) {
[...]
try {
// Extract OMID verification script, parameters from VAST
...
adSession = getNativeAdSession(this);
} catch (Exception e) {
// Call the 'verificationNotExecuted' tracker
// if any errors occur while preparing OMID session
emitBeacon(adVerificationNotExecuted);
}
mediaEvents = MediaEvents.createMediaEvents(adSession);
adEvents = AdEvents.createAdEvents(adSession);
adSession.registerAdView(playerView);
adSession.start();
[...]
}
[...]
}
4. Trigger adEvents & mediaEvents events to OM SDK when Video & Ad related events occur
adEvents & mediaEvents events to OM SDK when Video & Ad related events occurThrough those calls, the Criteo verification script will be able to compute advanced viewability events:
- Criteo will be able to track quartile events (if the video is 100% part of the viewport)
- Advanced events are computed to track if 50% of the video is playing & displayed in the viewport during 2 consecutive seconds.
All calls to both AdEvents & MediaEvents must be triggered aside of the existing tracking beacons for quartile & display events. Additionally, the video buffering events must be triggered.
Triggering OMID events is similar to triggering VAST trackers, both will be described in the following section.
Step 4: firing beacons during video playback
The first beacons we need to fire are the VAST impression tracking beacons(for the placement, for each product, and for the video from the VAST).
To set a trigger for this, we add an event listener to our player element for the STATE_READY event. This event is dispatched when the player is able to immediately play the video, regardless of its position in the viewport.
To be able to listen player state changes over time, the
VideoAdNativeActivityneeds to inherit fromPlayer.Listener. By implementingonPlayerStateChanged, the developer will get notified of video player related events
public class VideoAdNativeActivity extends Activity implements Player.Listener, View.OnClickListener {
[...]
private final OkHttpClient client = new OkHttpClient();
private final Callback emptyCallback = new Callback() {
@Override
public void onFailure(Call call, IOException e) { }
@Override
public void onResponse(Call call, Response response) { }
};
private void emitBeacon(String url) {
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(emptyCallback);
}
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
switch (playbackState) {
case ExoPlayer.STATE_READY:
onStateReady();
break;
case ExoPlayer.STATE_ENDED:
onStateEnded();
break;
case ExoPlayer.STATE_BUFFERING:
onStateBuffering();
break;
case ExoPlayer.STATE_IDLE:
break;
default:
throw new IllegalStateException("Unknown playbackState: " + playbackState);
}
}
private boolean loaded = false;
private void onStateReady() {
if (!loaded) {
emitBeacon(adImpression);
loaded = true;
// forward event to OMID
VastProperties vastProperties = VastProperties.createVastPropertiesForNonSkippableMedia(false, Position.STANDALONE);
adEvents.loaded(vastProperties);
adEvents.impressionOccurred();
}
// forward event to OMID
mediaEvents.bufferFinish();
// launch polling thread to monitor video progress
postProgress();
}
private void onStateBuffering() {
// forward event to OMID
mediaEvents.bufferStart();
}
[...]
}
To ensure accurate beacon tracking, playback beacons (start and quartiles beacons) should only be emitted once, during the first play of the video. It is important that the video maintains a state for each emitted beacon to prevent duplication.
However, interaction beacons (such as clicks, mute, unmute, pause, and resume) must be emitted each time the corresponding user action occurs.
The start, firstQuartile, midpoint, and thirdQuartile beacons are managed in a function triggered by a polling thread.
The onProgress method checks that the ad session is still alive and that the video hasn’t been already completed once, after which it calls the updateQuartile method, which calculates the current quartile and drops the corresponding beacon. It also calls the updatePlayPause method, which monitors the play and pause events.
Lastly, it launches the postProgress method, which closes the loop by queuing another execution of the ProgressRunnable.
public class VideoAdNativeActivity extends Activity implements Player.Listener, View.OnClickListener {
[...]
private final Handler handler = new Handler(Looper.getMainLooper());
private final ProgressRunnable progressRunnable = new ProgressRunnable(this);
private static final int PROGRESS_INTERVAL_MS = 100;
private static class ProgressRunnable implements Runnable {
private final WeakReference<VideoAdNativeActivity> videoAdNativeActivityWeakReference;
ProgressRunnable(VideoAdNativeActivity videoAdNativeActivity) {
this.videoAdNativeActivityWeakReference = new WeakReference<>(videoAdNativeActivity);
}
@Override
public void run() {
VideoAdNativeActivity videoAdNativeActivity = videoAdNativeActivityWeakReference.get();
if (videoAdNativeActivity == null) {
return;
}
videoAdNativeActivity.onProgress();
}
}
private void onProgress() {
if (adSession == null) {
return;
}
if (complete) {
return;
}
updateQuartile();
postProgress();
}
private void postProgress() {
handler.removeCallbacks(progressRunnable);
handler.postDelayed(progressRunnable, PROGRESS_INTERVAL_MS);
}
[...]
}
Inside this updateQuartile function, we check that the video ad hasn’t been completed yet. If so, we calculate the current playing quartile, check that it has not been sent as an event before and send it. So first we send the start beacon, then when 25% video duration mark has passed we send firstQuartile, then midpoint and finallythirdQuartile.
// updateQuartile method does some calculations to get the currently playing quartile,
// executes some safety checks, and triggers the quartile event.
private Quartile lastSentQuartile = Quartile.UNKNOWN;
private void updateQuartile() {
final long duration = player.getDuration();
final long currentPosition = player.getCurrentPosition();
if (duration != 0 && !complete) {
final Quartile currentQuartile = getQuartile(currentPosition, duration);
// Don't send old quartile stats that we have either already sent, or passed.
if (currentQuartile != lastSentQuartile && currentQuartile.ordinal() > lastSentQuartile.ordinal()) {
sendQuartile(currentQuartile);
lastSentQuartile = currentQuartile;
}
}
}
private void sendQuartile(Quartile quartile) {
switch (quartile) {
case START:
// Fire start beacon
emitBeacon(adStarted);
// Forward event to OMID
mediaEvents.start(player.getDuration(), PLAYER_VOLUME);
break;
case FIRST:
// Fire firstQuartile beacon
emitBeacon(adFirstQuartile);
// Forward event to OMID
mediaEvents.firstQuartile();
break;
case SECOND:
// Fire midpoint beacon
emitBeacon(adMidpoint);
// Forward event to OMID
mediaEvents.midpoint();
break;
case THIRD:
// Fire thirdQuartile beacon
emitBeacon(adThirdQuartile);
// Forward event to OMID
mediaEvents.thirdQuartile();
break;
case UNKNOWN:
default:
break;
}
}
// Enum to represent quartiles and some helper methods to round the video position to the nearest completed quartile
public enum Quartile { UNKNOWN, START, FIRST, SECOND, THIRD }
// A way to detect when the quartile events occur
private static Quartile getQuartile(long position, long duration) {
final double completionFraction = position / (double) duration;
if (lessThan(completionFraction, 0.01)) {
return Quartile.UNKNOWN;
}
if (lessThan(completionFraction, 0.25)) {
return Quartile.START;
}
if (lessThan(completionFraction, 0.5)) {
return Quartile.FIRST;
}
if (lessThan(completionFraction, 0.75)) {
return Quartile.SECOND;
}
// We report Quartile.THIRD when completionFraction > 1 on purpose
// since track might technically report elapsed time after its completion
// and if Quartile.THIRD hasn't been reported already, it will be lost
return Quartile.THIRD;
}
private static boolean lessThan(double a, double b) {
return b - a > EPSILON;
}
To handle the complete beacon, we listen for the ended event fired by the video player. Upon receiving this event, we check if the video hasn’t completed yet and, and if so, we drop the complete beacon.
Afterward, we set completed to true and call play() on the video to create the looping effect. Subsequent "ended" events should only restart the video.
private void onStateEnded() {
if (!complete) {
emitBeacon(adCompleted);
// Forward event to OMID
mediaEvents.complete();
complete = true;
}
// Restart the video
player.seekTo(0);
player.play();
}All playback beacons are fired only when the player is playing.
To ensure the ad is visible per IAB standards, at least 50% of the video player must be in the viewport. We use a listener on the UI thread that is called when the UI is about to be drawn, in this listener we check if the player is at least 50% in the viewport, and pause the video if it is not.
private void initializePlayer(String videoUrl) {
[...]
playerView.getViewTreeObserver().addOnPreDrawListener(() -> {
// Check if at least 50% of the video is visible
// And that the user didn't explicitly pause the playback
if (isPlayingWhen50Visible && isVideoViewAtLeast50Visible(playerView)) {
playVideo();
} else {
pauseVideo();
}
return true;
});
}
private boolean isVideoViewAtLeast50Visible(View videoView) {
// Get the visible rectangle of the view
Rect visibleRect = new Rect();
boolean isVisible = videoView.getGlobalVisibleRect(visibleRect);
if (!isVisible) {
return false;
}
// Get total area of the video view
int totalArea = videoView.getWidth() * videoView.getHeight();
if (totalArea == 0) {
return false;
}
// Get the visible area
int visibleArea = visibleRect.width() * visibleRect.height();
return (visibleArea >= totalArea / 2);
}
We handle the ClickTracking beacon by attaching an event listener to our Video player for the click event. In the handler function, we drop the beacon and instruct the browser to open the ClickThrough URL from the VAST tag, only if a redirection URL is provided.
If the redirection URL is not provided, we pause/resume the video and trigger the corresponding tracking beacon
public class VideoAdNativeActivity extends Activity implements Player.Listener, View.OnClickListener {
[...]
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.videoView:
if (clickThroughUrl != null) {
clickThroughHandler();
} else {
handlePlayPause();
}
break;
case R.id.muteTextView:
toggleMute(mediaEvents, player, muteTextView);
break;
}
}
public void clickThroughHandler() {
// Emit the click beacon
if (adClickThrough != null) {
emitBeacon(adClickThrough);
}
// Redirect the user to the ClickThrough URL
try {
Intent externalIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(clickThroughUrl));
this.startActivity(externalIntent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
}
private boolean isPlayingWhen50Visible = true;
public void handlePlayPause() {
if (isPlaying) {
pauseVideo();
emitBeacon(adPause);
} else {
playVideo();
emitBeacon(adPlay);
}
isPlayingWhen50Visible = !isPlayingWhen50Visible;
}
private void playVideo() {
player.play();
// Forward event to OMID
mediaEvents.resume();
}
private void pauseVideo() {
player.pause();
// Forward event to OMID
mediaEvents.pause();
}
[...]
}
Finally, we handle the mute button similarly, by attaching a listener to it.
It changes the player volume and triggers the tracking beacons.
public void toggleMute() {
int PLAYER_MUTE = 0;
int PLAYER_UNMUTE = 1;
float currentVolume = player.getVolume();
float targetVolume;
if (currentVolume == PLAYER_MUTE) {
targetVolume = PLAYER_UNMUTE;
muteTextView.setText("Mute");
emitBeacon(adUnmute);
} else {
targetVolume = PLAYER_MUTE;
muteTextView.setText("Unmute");
emitBeacon(adMute);
}
player.setVolume(targetVolume);
// Forward event to OMID
mediaEvents.volumeChange(targetVolume);
}
Step 5: finishing the ad session
It is important to handle correctly the destruction of the player and the ad session.
public class VideoAdNativeActivity extends Activity implements Player.Listener, View.OnClickListener {
[...]
@Override
public void onDestroy() {
super.onDestroy();
adSession.finish();
adSession = null;
handler.removeCallbacksAndMessages(null);
releasePlayer();
}
private void releasePlayer() {
if (player != null) {
player.release();
player = null;
}
}
[...]
}
Extra: Handling closed captions (CC)
To add the closed captions, you need to provide a SubtitleConfiguration object that contains the CC URL to the MediaItem.Builder:
private void initializePlayer(String videoUrl, String ccUrl) {
[...]
// Create the MediaItem to play
MediaItem.Builder mediaItemBuilder =
new MediaItem.Builder()
.setUri(Uri.parse(videoUrl));
if (ccUrl != null) {
// Set your VTT subtitle URI
Uri subtitleUri = Uri.parse(ccUrl);
// Prepare the subtitle configuration
MediaItem.SubtitleConfiguration subtitleConfig = new MediaItem.SubtitleConfiguration.Builder(subtitleUri)
.setMimeType(MimeTypes.TEXT_VTT) // The MIME type for WebVTT subtitles
.setLanguage("en") // Optional: Specify the language
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT) // Optional: Set flags like default
.build();
mediaItemBuilder.setSubtitleConfigurations(Collections.singletonList(subtitleConfig));
}
MediaItem mediaItem = mediaItemBuilder.build();
[...]
}
NoteThe link for the CC is provided inside VAST
ClosedCaptionFiletag
Updated 3 months ago
