Appearance
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:
| Example | Ad source | Midroll behavior |
|---|---|---|
| Manual CSAI | VAST tags from a bundled JSON ad break | The app schedules and plays the ad pod itself |
| Google IMA CSAI | A hosted VAST request | Google IMA requests and sequences client-side ads |
| Google IMA SSAI | A Google DAI VOD stream | Google 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 assembleDebugThen 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.
- Before content starts, the app fetches each Infillion VAST tag from the ad break fixture (
manual_ad_break.json) and resolves itsadParameters. - At the midroll, the app saves the content position and plays the ads of the pod one by one in the same player.
- For a TrueX or IDVx ad, the app does not play the placeholder video: it stops and hides the player and starts
TruexAdRendererin the overlay container. - On
AD_COMPLETEDafterAD_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. - 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.
- On the IMA
STARTEDevent, the app checksad.adSystem. A TrueX ad that is not the first in the pod (adPodInfo.adPosition != 1) plays as a normal linear ad. - For an Infillion ad, the app resolves
adParametersfromad.companionAds/ad.traffickingParameters, pauses theAdsManagerand the player, and starts the renderer. - On
AD_COMPLETEDafterAD_FREE_POD, the app callsadsManager.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.
- On the IMA
STARTEDevent, the app tracks where the current placeholder ad ends in the stream (adPodInfo.timeOffsetplus the ad durations), and checksad.adSystemand the pod position like the CSAI example. - For an Infillion ad, the app resolves
adParameters, pauses and hides the stream, and starts the renderer. - Google DAI has no
discardAdBreak(), so the app moves the timeline by seeking. OnAD_COMPLETEDafterAD_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

