Video player implementation (App: Android)

⚠️

Hidden page

This 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 iOS

This 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 Beta

The 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 Documentation

For 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" and targetSdkVersion="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:

  1. Requesting and parsing the VAST tag,
  2. Creating the HTML structure of your player to be injected into the DOM,
  3. Firing beacons at the proper time during video playback,
  4. 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();
⚠️

dBuilder queries 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 Android

Learn 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 OMID

The 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 SDK

To 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 & mediaEvents events 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:

LevelBeacon nameDescription
OMID (VerificationParameters)omidTrackViewIndicates that the OMID Ad session was properly initialized..
OMID (VerificationParameters)startIndicates that the video just started, and the video is displayed 100% in the viewport.
OMID (VerificationParameters)firstQuartileIndicates that the video just reached the 25% quartile, and the video is displayed 100% in the viewport.
OMID (VerificationParameters)midpointIndicates that the video just reached the 50% quartile, and the video is displayed 100% in the viewport.
OMID (VerificationParameters)thirdQuartileIndicates that the video just reached the 75% quartile, and the video is displayed 100% in the viewport.
OMID (VerificationParameters)completeIndicates that the video just completed, and the video is displayed 100% in the viewport.
OMID (VerificationParameters)twoSecondsFiftyPercentViewIndicates 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 , VerificationParameters and VendorKey are 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 verificationNotExecuted VAST 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

🔗

The complete list of available AdEvents & MediaEvents is accessible on IAB’s Github.

Through 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 VideoAdNativeActivity needs to inherit from Player.Listener. By implementing onPlayerStateChanged , 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();
  
    [...]
}
📘

Note

The link for the CC is provided inside VAST ClosedCaptionFile tag