Skip to content

TV Examples ​

The infillion-ads-android-examples repository contains kotlin-ctv-app, an Android TV app with three complete integrations of TrueX and IDVx ads into a Media3 player:

ExampleAd sourceMidroll behavior
Manual CSAIVAST tags from a bundled JSON ad breakThe app schedules and plays the ad pod itself
Google IMA CSAIA hosted VAST requestGoogle IMA requests and sequences client-side ads
Google IMA SSAIA Google DAI VOD streamGoogle DAI returns one stream with stitched ad breaks

Each example has its own player, ad, and TruexAdRenderer code on purpose, so you can pick the one closest to your setup and read it from top to bottom.

Running the App ​

Requirements:

  • Android Studio with JDK 17
  • Android SDK 35
  • An Android TV device or emulator running API 28 or newer

Open the repository in Android Studio and run the kotlin-ctv-app configuration, or build it from the command line:

bash
./gradlew assembleDebug

Then install kotlin-ctv-app/build/outputs/apk/debug/kotlin-ctv-app-debug.apk on the device. Use the D-pad to pick an example; each one shows its current state (content, linear ad, interactive ad, errors) in a status panel.

Test IP parameter

The sample VAST tags include &ip=158.106.195.210, the IP of the Infillion NYC office. The TrueX ad server serves ads in the US and Canada only, and this parameter lets teams and CI runners outside those regions get filled test ads. Do not use it in production.

Resolving adParameters ​

All three examples resolve adParameters in the same order - the truex companion first, then <AdParameters> - as described in Integration Flow. With Google IMA (CSAI and DAI) the resolver looks like this:

kotlin
fun extractAdParameters(
    companionAds: List<CompanionAd>?,
    traffickingParameters: String?,
): JSONObject? {
    // 1. Companion tag: base64 JSON data URL in <StaticResource>
    companionAds?.forEach { companion ->
        if (companion.apiFramework.equals("truex", ignoreCase = true)) {
            val resource = companion.resourceValue.filterNot { it.isWhitespace() }
            val json = runCatching {
                String(Base64.getDecoder().decode(resource.substringAfter(',')), Charsets.UTF_8)
            }.getOrNull()
            val parsed = json?.let { runCatching { JSONObject(it) }.getOrNull() }
            if (parsed != null) {
                return parsed
            }
        }
    }
    // 2. Generic tag: <AdParameters>, exposed by IMA as traffickingParameters
    if (!traffickingParameters.isNullOrBlank()) {
        return runCatching { JSONObject(traffickingParameters) }.getOrNull()
    }
    return null
}
java
@Nullable
static JSONObject extractAdParameters(
        @Nullable List<CompanionAd> companionAds,
        @Nullable String traffickingParameters) {
    // 1. Companion tag: base64 JSON data URL in <StaticResource>
    if (companionAds != null) {
        for (CompanionAd companion : companionAds) {
            if (!"truex".equalsIgnoreCase(companion.getApiFramework())) {
                continue;
            }
            String resource = companion.getResourceValue().replaceAll("\\s", "");
            String encoded = resource.substring(resource.indexOf(',') + 1);
            try {
                byte[] decoded = Base64.getDecoder().decode(encoded);
                return new JSONObject(new String(decoded, StandardCharsets.UTF_8));
            } catch (IllegalArgumentException | JSONException e) {
                // Not a valid companion payload, try the next source
            }
        }
    }
    // 2. Generic tag: <AdParameters>, exposed by IMA as traffickingParameters
    if (traffickingParameters != null && !traffickingParameters.trim().isEmpty()) {
        try {
            return new JSONObject(traffickingParameters);
        } catch (JSONException e) {
            return null;
        }
    }
    return null;
}

The Manual CSAI example does the same with the raw VAST XML, reading <Companion apiFramework="truex"> / <StaticResource> and <AdParameters> directly - see ManualVastPayload.kt.

Manual CSAI ​

Use this example when your app or your own ad layer decides when an ad break starts and which ads it contains.

  1. Before content starts, the app fetches each Infillion VAST tag from the ad break fixture (manual_ad_break.json) and resolves its adParameters.
  2. At the midroll, the app saves the content position and plays the ads of the pod one by one in the same player.
  3. For a TrueX or IDVx ad, the app does not play the placeholder video: it stops and hides the player and starts TruexAdRenderer in the overlay container.
  4. On AD_COMPLETED after AD_FREE_POD, the app ends the pod and returns to the content. Otherwise (opt-out, error, no ads, or IDVx) the next ad in the pod starts.
  5. After the pod, the content is restored at its saved position.

Files: ManualCsaiActivity.kt, ManualVastPayload.kt, ManualAdBreak.kt - guide: manual-csai.md

Google IMA CSAI ​

Use this example when Google IMA requests and plays your client-side ads.

  1. On the IMA STARTED event, the app checks ad.adSystem. A TrueX ad that is not the first in the pod (adPodInfo.adPosition != 1) plays as a normal linear ad.
  2. For an Infillion ad, the app resolves adParameters from ad.companionAds / ad.traffickingParameters, pauses the AdsManager and the player, and starts the renderer.
  3. On AD_COMPLETED after AD_FREE_POD, the app calls adsManager.discardAdBreak() and resumes. Otherwise it finishes the placeholder ad so IMA continues with the next ad in the pod.
kotlin
private fun finishInteractive(shouldSkipPod: Boolean) {
    disposeRenderer()
    binding.playerView.visibility = View.VISIBLE
    if (shouldSkipPod) {
        // TrueX reward earned: discard the rest of the client-side ad break
        adsManager?.discardAdBreak()
    } else {
        // Opt-out, error, or IDVx: finish the placeholder and play the next ad
        videoPlayer.finishCurrentAd()
    }
    adsManager?.resume()
}
java
private void finishInteractive(boolean shouldSkipPod) {
    disposeRenderer();
    playerView.setVisibility(View.VISIBLE);
    if (shouldSkipPod) {
        // TrueX reward earned: discard the rest of the client-side ad break
        adsManager.discardAdBreak();
    } else {
        // Opt-out, error, or IDVx: finish the placeholder and play the next ad
        videoPlayer.finishCurrentAd();
    }
    adsManager.resume();
}

Files: ImaCsaiActivity.kt, ImaCsaiAdPayload.kt, ImaCsaiVideoPlayer.kt - guide: ima-csai.md

Google IMA SSAI ​

Use this example when Google IMA DAI returns a single stream with stitched ad breaks.

  1. On the IMA STARTED event, the app tracks where the current placeholder ad ends in the stream (adPodInfo.timeOffset plus the ad durations), and checks ad.adSystem and the pod position like the CSAI example.
  2. For an Infillion ad, the app resolves adParameters, pauses and hides the stream, and starts the renderer.
  3. Google DAI has no discardAdBreak(), so the app moves the timeline by seeking. On AD_COMPLETED after AD_FREE_POD, it seeks past the whole ad break. Otherwise it seeks to just before the end of the placeholder ad, so the next stitched ad plays.
kotlin
private fun finishInteractive(shouldSkipPod: Boolean) {
    disposeRenderer()
    videoStreamPlayer.show()
    val pod = streamManager?.currentAd?.adPodInfo
    val progress = streamManager?.adProgressInfo
    if (shouldSkipPod && pod != null && progress != null) {
        // TrueX reward earned: seek past the whole stitched ad break
        val breakEndMs = ((pod.timeOffset + progress.adBreakDuration + 2.0) * 1_000).toLong()
        videoStreamPlayer.seekTo(breakEndMs)
    } else {
        // Opt-out, error, or IDVx: seek to the placeholder end, the next stitched ad plays
        videoStreamPlayer.seekTo((currentAdEndMs - 100L).coerceAtLeast(0L))
    }
    videoStreamPlayer.resume()
}
java
private void finishInteractive(boolean shouldSkipPod) {
    disposeRenderer();
    videoStreamPlayer.show();
    Ad currentAd = streamManager != null ? streamManager.getCurrentAd() : null;
    AdPodInfo pod = currentAd != null ? currentAd.getAdPodInfo() : null;
    AdProgressInfo progress = streamManager != null ? streamManager.getAdProgressInfo() : null;
    if (shouldSkipPod && pod != null && progress != null) {
        // TrueX reward earned: seek past the whole stitched ad break
        long breakEndMs = (long) ((pod.getTimeOffset() + progress.getAdBreakDuration() + 2.0) * 1_000);
        videoStreamPlayer.seekTo(breakEndMs);
    } else {
        // Opt-out, error, or IDVx: seek to the placeholder end, the next stitched ad plays
        videoStreamPlayer.seekTo(Math.max(currentAdEndMs - 100L, 0L));
    }
    videoStreamPlayer.resume();
}

Files: ImaSsaiActivity.kt, ImaSsaiAdPayload.kt, ImaSsaiVideoStreamPlayer.kt - guide: ima-ssai.md

Integration documentation for Infillion Ads