Companion ads
A companion ad is a banner that appears next to the video player while an ad
plays, for example, a brand logo with a "Watch now" button, a swipeable brand
carousel, or a standard IMA banner. The fl-player module provides
CompanionPresentation to show these companions. Your app supplies the
container view, decides what happens when a viewer taps a companion, and
receives events for analytics. This guide also covers contextual ads scheduled
with playhead boundaries and takeover ads that your app parses and renders;
those flows aren't formats managed by CompanionPresentation.
For iOS, see iOS companion ads. For web, see Web companion ads.
Schedule contextual ads
Use the Android Player boundary observer to schedule app-owned contextual ad
rendering at stream-time positions. The observer reports timing only; your app
parses the contextual ad data and renders the ad.
This API is available starting with Android SDK 7.1.137.
val boundaryToken = player.addPlayheadBoundaryObserver(
positionsMs = listOf(30_000L, 60_000L),
lookaheadMs = 1_000L
) { positionMs ->
// Coordinate rendering the contextual ad for this stream-time position.
}
positionsMs and the callback value use milliseconds. lookaheadMs defaults
to 0 and must be zero or greater; it moves the callback earlier than the
requested boundary. The callback runs on the main thread. Retain the returned
token for the playback and remove the observer when playback completes or
stops:
boundaryToken?.let { player.removeBoundaryObserver(it) }
The method returns null if it can't register the observer or no valid
positions remain. It discards negative positions, removes duplicates, and
sorts the remaining positions. On a forward seek, a callback fires only when
the seek destination matches a registered position to second-level precision;
backward seeks don't trigger callbacks.
Implement takeover ads in the application
Takeover ads aren't a CompanionPresentation format. Your app must parse the
ad metadata and own takeover rendering; the SDK doesn't parse takeover
parameters or create a takeover WebView.
| Takeover type | App input | App responsibility |
|---|---|---|
| In-video | IMA ad events and VAST traffickingParameters | Parse the takeover fields, then show and dismiss the takeover with the ad lifecycle. |
| Contextual | VOD cue list or live SCTE-35 signal and contextual ad metadata | Parse the contextual data and render it at the intended stream-time position. |
| Ads under a tab | Details-page ad data | Parse and render the ad in the app's page UI; this doesn't use the player companion container. |
The takeover design uses different metadata for each type. These are ad-server/application conventions, not companion SDK parameters:
| Takeover type | Source | Keys or fields |
|---|---|---|
| In-video | VAST traffickingParameters | ad_type=webview, campaign, and Image_url (the page URL despite its name). |
| Contextual | GAM native custom format | ad_type=takeoverAds and Takeover_ads_url. |
| Ads under a tab, GAM | GAM native custom format | ad_type=takeoverAds, Takeover_ads_url, redirection_url, sidelogo, c_title, thirdPartyImpression, thirdPartyClick, and advertiser_name. |
| Ads under a tab, backend | Details-page backend data | AdvertiserTab.adType and takeover_ads_url. |
For example, the in-video VAST parameters may look like this:
ad_type=webview,campaign=<campaign-name>,Image_url=<https-url>
Parse parameters by key rather than relying on their order, and split each
key-value entry at its first = so URLs with query strings remain intact.
Validate destination URLs according to your app's security policy before
loading or opening them. The app also owns WebView lifecycle, click handling,
and takeover analytics.
Supported companion types
| Companion type | Supported | Ad delivery | Rendered by | Notes |
|---|---|---|---|---|
| Carousel | Yes | Any provider, through trafficking parameters | SDK | Portrait only. Collapsed banner that expands into up to 20 images. Can be retained after the ad. |
| Custom CTA | Yes | Any provider, through trafficking parameters | SDK | Icon, title, optional description, and one button. Never retained after the ad. |
| Native IMA banner | Yes | IMA CSAI with VAST companions | IMA, in an SDK-owned slot | One fixed size per presentation. Not available with IMA DAI or Datazoom MediaTailor. |
| Static image (SSAI) | No | Not applicable | Not applicable | Use carousel or custom CTA creatives for SSAI ads. |
| HTML, iframe, or takeover | No | Not applicable | Your app | The SDK draws only the carousel, the custom CTA, or the native banner. |
For the web equivalent, see Web companion ads.
Choose a companion format
The presentation shows one companion at a time. Each ad can use one of three formats, depending on the ad's parameters and the provider.
| Format | When to use it | Where the creative comes from | Who renders it | Who tracks it |
|---|---|---|---|---|
| Carousel | You want a collapsed brand banner that expands into a gallery of images. | Ad trafficking parameters | The SDK | Your app, through onEvent |
| Custom CTA | You want a branded row with an icon, copy, and a button. | Ad trafficking parameters | The SDK | Your app, through onEvent |
| Native IMA banner | You serve ads with Google IMA and have VAST companions. | IMA VAST companions | IMA, into an SDK-owned slot | IMA |
When more than one format is available for an ad, the presentation picks one using these rules:
- A carousel takes precedence over a custom CTA. The two never show together.
- A carousel or custom CTA takes precedence over a native banner. Nothing shows while their images load, so a native banner doesn't flash in first.
- If a required image fails, the native banner can take its place.
- The carousel is a portrait format. In landscape, the native banner covers the space instead.
Your app, not the SDK, is responsible for:
- Page layout, orientation, and picture-in-picture (PiP) behavior. Use
presentationModeto override the window orientation policy when your player layout needs it. - Deep links and operations such as watchlist or reminders. Route them through
CompanionActions. - Takeover rendering. The SDK draws only the carousel, the custom CTA, or the native banner inside the container.
Before you begin
- Include the
fl-playermodule in your app. - For native IMA banners, use IMA CSAI. Native slots aren't available with IMA DAI or Datazoom MediaTailor.
- Create the presentation on the main thread. Every
CompanionPresentationcall must run on the main thread.
The native banner slot is only used with IMA CSAI. Carousel and custom CTA creatives work with every provider when the ad carries trafficking parameters.
Create the companion presentation
Create the presentation before the ads player, and pass it to the factory. The
factory prepares the slots and binds the listeners before load(). Keep one
presentation for the lifetime of the screen, and release it when the screen is
destroyed.
val companionPresenter = CompanionPresentation(
container = binding.companionContainer,
standardSlotSize = CompanionSlotSize.Fixed(320, 50), //size 728,90 for Tablet
configuration = CompanionConfiguration(
persistCarouselAfterAd = backendFlags.persistCarousel
),
actions = CompanionActions { action ->
// Your implementation here
CompanionActionResult.Completed
},
onEvent = { event ->
//handle event
}
)
val adPlayer = IMAAdsPlayerFactory.imaAdComposedPlayerWith(
contentPlayer = contentPlayer,
adRequest = CSAIAdsRequest(adTagUrl = adTagUrl),
companionPresenter = companionPresenter
)
Keep your usual composablePlayerWith, attachRendererView, and playback
setup.
| Parameter | Required | Description |
|---|---|---|
container | Yes | The ViewGroup the SDK draws the companion into. Use a container whose height can wrap its content. |
standardSlotSize | No | A CompanionSlotSize.Fixed(widthPx, heightPx) for the native IMA banner. Sizes are pixels. Omit it to show only the carousel and custom CTA. |
configuration | No | Formats, theme, retention, and timeouts. See Configuration reference. |
actions | No | A CompanionActions callback that handles taps. See Handle taps. |
onEvent | No | Receives CompanionEvent values. See Listen to events. |
CompanionSlotSize.Fluid is rejected. Use a fixed size for the native banner.
Manage the presentation
Use these members across content changes and for teardown:
companionPresenter.presentationMode = CompanionPresentationMode.Fullscreen
companionPresenter.updateConfiguration(updatedConfiguration)
companionPresenter.onContentReplaced() // Call before reusing it for a replaced content item
companionPresenter.release() // Final teardown
Call release() when the screen goes away, before you stop playback or remove
the container. Release is idempotent and final. A temporary view detachment,
such as a configuration change that recreates a view, isn't a release. Create
a new presentation for a new screen.
If your playlist integration can't report content replacement reliably, call
onContentReplaced() before you reuse the presentation. No content ID is
required. A new ad, content replacement, stop, stream end, or release clears
state and cancels pending requests.
Set up a custom CTA
A custom CTA is one compact row with a brand icon, a title with optional
copy, and one action button. The SDK builds it from the ad's trafficking
parameters, which are a comma-separated list of key=value pairs.
icon=https://cdn.example.com/brand-64.png,bannerTitle=Order groceries in minutes,bannerDescription=Free delivery on your first order,ctaText=Order now,ctaUrl=https://www.example.com/offer,campaign=spring-groceries,bgColour=#1A1A1A,bannerTextColor=#FFFFFF,ctaBgColor=#E53935,ctaTextColor=#FFFFFF
| Parameter | Required | Description |
|---|---|---|
icon or Icon | Yes | HTTP(S) URL of the icon image. Without it the ad has no CTA, and no rejection reason is reported. |
bannerTitle | Yes | Title text. It can't be blank. |
ctaUrl | Yes | Destination. Any scheme is accepted. |
ctaText | No | Button label. Defaults to Learn more. |
bannerDescription | No | Copy below the title. |
bgColour | No | Surface color. |
bannerTextColor | No | Title and copy color. |
ctaBgColor, cta_bg_color | No | Button background color. |
ctaTextColor, cta_text_color | No | Button text color. |
campaign, campaign name | No | Campaign name included in event campaign fields. |
companion_schema_version | No | Only 1 is accepted. If omitted, 1 is used. |
Keep these rules in mind when you write the parameters:
- Parameter names are case-insensitive.
- Conflicting aliases reject the creative. For example,
iconandIconwith different values, orcampaignandcampaign namewith different values. - Duplicate keys, malformed segments, and an unsupported schema version reject the whole creative.
- Split on the first
=only, so URL query strings are preserved. Literal commas have no escape syntax. - A rejected creative never shows. The reason is reported in the
DisplayDecisionevent, and a native banner can show instead.
The default tap handler opens HTTP and HTTPS URLs only. For any other scheme,
the button is disabled unless you supply a CompanionActions callback.
A custom CTA never stays on screen after the ad ends.
Set up a carousel
The carousel is a swipeable gallery of brand images. It starts as a collapsed banner. A tap on the banner expands it, and the close button collapses it.
Collapsed Expanded
+--------------------+ +---------------------------------+
| carousel_banner | | [icon] Title [x] |
+--------------------+ | Description |
| +--------+ +--------+ +--------+ |
| | image1 | | image2 | | image3 | |
| +--------+ +--------+ +--------+ |
| [ SHOP ] |
+---------------------------------+
The carousel appears after the banner, the brand icon, and the first image load. The remaining images stream in while it's expanded. Each card is 220 dp wide and keeps its aspect ratio. A single image fills the width. Expansion and scroll position survive rotation within one ad.
| Parameter | Required | Description |
|---|---|---|
ad_type | Yes | Must be carousel. |
carousel_banner | Yes | HTTP or HTTPS image URL for the collapsed banner. |
brand_icon | Yes | HTTP or HTTPS image URL for the expanded header. |
carousel_title | Yes | Nonblank text. |
carousel_description | Yes | The key must exist. The value may be empty. |
carousel_images | Yes | $-separated HTTP or HTTPS image URLs. At least one must be usable. |
carousel_images_urls | No | $-separated tap targets, matched to the images by position. |
carousel_cta_text | No | Label of the shared button. Needs carousel_cta_url. |
carousel_cta_url | No | HTTP or HTTPS target of the shared button. Needs carousel_cta_text. |
carousel_cta_bg_color | No | Shared button background color. |
carousel_cta_textColor | No | Shared button text color. |
campaign, campaign name | No | Campaign name included in event campaign fields. |
companion_schema_version | No | Only 1 is accepted. |
ad_type=carousel,carousel_banner=https://cdn.example.com/banner.png,brand_icon=https://cdn.example.com/brand-64.png,carousel_title=Dunzo: Grocery Shopping & More,carousel_description=,carousel_cta_text=SHOP,carousel_cta_url=https://www.example.com/shop,carousel_images=https://cdn.example.com/1.png$https://cdn.example.com/2.png$https://cdn.example.com/3.png,carousel_images_urls=
These rules apply to the image lists:
- A carousel has at most 20
carousel_imagesentries. More than 20 reject the whole carousel. - An entry that isn't a valid HTTP or HTTPS URL is dropped. The remaining
images keep their original positions for
ItemChangedand accessibility labels. - A blank entry in
carousel_images_urlsleaves that image inert. - The shared button needs both
carousel_cta_textandcarousel_cta_url. When the shared button exists, image taps are disabled.
Your app controls the carousel through these settings:
formatsmust containCompanionFormat.CAROUSELfor the carousel to show.persistCarouselAfterAdkeeps a completed carousel on screen after the ad. See Keep a carousel after the ad ends.themesupplies the surface and text colors. The carousel ignoresbgColour.
The carousel is a portrait format. In landscape, or when the carousel format is disabled, or when a required image fails, the native banner takes its place.
Set up the native IMA banner
The native IMA banner is the fallback format. IMA draws it into a slot that the presentation owns, and your ad server controls its creative. You don't configure it through ad parameters.
To enable it:
- Pass a fixed
standardSlotSizewhen you create the presentation. - Keep
CompanionFormat.STANDARDinformats. It is included by default. - Use IMA CSAI through
IMAAdsPlayerFactory.imaAdComposedPlayerWith.
The banner shows only during an active ad, only when no carousel or custom CTA applies, and only when it fits the container. It is hidden before, between, and after ads. IMA owns its rendering, clicks, and beacons, so you don't dispatch any tracking for it.
A managed presentation supports one native size. The size can't change during a native session. Create a new presentation for a new size.
If an ad has no native companion, the slot stays empty and reserves its fixed height while the ad plays.
Handle taps
The SDK calls the CompanionActions callback when a viewer taps a custom CTA,
a carousel image, or the carousel button. Pass a CompanionActions lambda or
implementation to the constructor.
val actions = CompanionActions { action ->
when {
action.url.startsWith("$url") -> {
router.open(action.url)
CompanionActionResult.Completed
}
else -> CompanionActionResult.Unsupported
}
}
execute is a suspend function. It receives a CompanionAction with the
url to open, and it returns one of the following results:
| Return value | Result |
|---|---|
CompanionActionResult.Completed | Your app handled the action. |
CompanionActionResult.Unsupported | Your app doesn't handle this URL. The SDK opens HTTP and HTTPS URLs in the default handler. Other schemes stay disabled. |
CompanionActionResult.Failed(reason) | The action failed. An ActionFailed event is emitted with the reason. |
When you supply a callback, it handles every action. Return Unsupported for
URLs you don't own.
Keep these behaviors in mind:
- A button is enabled when a
CompanionActionscallback exists, or when the URL is HTTP or HTTPS. - Pending taps are deduplicated. Repeated taps are ignored while an action is running.
- Each action times out after
actionTimeoutMs. A timeout can leave a backend change in an unknown state, so reconcile it in your app and use backend idempotency where available. - If the callback completes after the ad has ended or the presentation was cleared, the result is dropped.
Control visibility and lifecycle
The presentation follows the ad's lifecycle automatically. The container and its
views are owned by the SDK until you call release().
| Signal | Behavior |
|---|---|
| Ad start | Clears the previous creative and action state, then decodes and loads the new ad. |
Ad end (AD_ENDED_EVENT) | Removes the carousel or custom CTA. Keeps a carousel only if persistence is on. |
Ad skip (AD_SKIPPED_EVENT), ad error | Clears everything, regardless of retention. |
| Content replaced, stop, or stream end | Clears everything and cancels pending work. |
release() | Unsubscribes, cancels downloads and actions, and removes the SDK views from the container. |
When the presentation is not visible to the viewer, for example during PiP or
another ad UI, your app owns the layout and the competing-presentation rules.
The presentation reports visibility changes through VisibilityChanged.
Keep a carousel after the ad ends
By default, a companion disappears when its ad ends. To keep a completed
carousel on screen until the next ad, set persistCarouselAfterAd to true.
It defaults to false.
A retained carousel stays until the next ad, a content change, a skip, an error, or teardown. Keep in mind:
- A custom CTA is never retained.
- The ad must complete. A skipped or errored ad isn't retained.
- Main content must resume playing before the carousel is retained.
- The carousel images must be usable.
- A
Retainedevent fires when the carousel becomes retained. - Turning persistence off with
updateConfigurationclears a retained carousel.
Listen to events
onEvent receives a sealed CompanionEvent. Every event carries the visible
format and the creative's optional campaign.
| Event | When it fires |
|---|---|
DisplayDecision | An ad starts. It reports the reason for the first decision. |
CapabilityLimited | The provider can't render native companions, for example with IMA DAI or Datazoom MediaTailor, while a managed standardSlotSize is set. |
Cleared | An ad is skipped, errors, ends playback, or content changes. |
Retained | A carousel becomes retained after the ad. |
ExpansionChanged | The user expands or collapses the carousel. |
VisibilityChanged | The visible format changes on screen. |
ItemChanged | The centered carousel image changes. |
AssetFailed | A required image fails to load. |
ActionRequested, ActionSucceeded, ActionFailed | A tap runs an action and the action finishes. |
onEvent = { event -> analytics.track(event) }
These events are app analytics. They don't replace the native IMA impression beacons, and the SDK doesn't create any tracking pixels for carousel or CTA creatives.
Configuration reference
CompanionConfiguration controls behavior. Update it at any time with
updateConfiguration. Both timeouts must be positive.
| Option | Default | Description |
|---|---|---|
persistCarouselAfterAd | false | Keep a completed carousel on screen after the ad. |
formats | STANDARD, CAROUSEL, CUSTOM_CTA | Formats the presentation may choose. |
theme | CompanionTheme() | Fallback surface, text, and button colors. |
actionTimeoutMs | 10000 | Time allowed for CompanionActions.execute to settle. |
imageTimeoutMs | 10000 | Time allowed for an image download. |
CompanionTheme takes optional color values for the fallback colors:
| Option | Description |
|---|---|
surfaceColor | Background color of the creative. |
textColor | Title and copy color. |
buttonColor | Button background color. |
buttonTextColor | Button text color. |
Colors must be #RRGGBB, or #AARRGGBB with an alpha of FF. The SDK ignores
translucent or malformed values. If text contrast is below 4.5:1, the SDK falls
back to black or white. Colors resolve in this order: the ad's parameters, then
CompanionTheme, then the host app's theme.
val configuration = CompanionConfiguration(
persistCarouselAfterAd = true,
formats = setOf(CompanionFormat.CAROUSEL, CompanionFormat.CUSTOM_CTA),
theme = CompanionTheme(
surfaceColor = 0xFF1A1A1A.toInt(),
textColor = 0xFFFFFFFF.toInt(),
buttonColor = 0xFFE53935.toInt(),
buttonTextColor = 0xFFFFFFFF.toInt()
),
actionTimeoutMs = 10_000,
imageTimeoutMs = 10_000
)
Image limits
The SDK bounds image work so one ad can't use unbounded memory:
- A download larger than 8 MiB fails.
- Large images are scaled down until each side is 1,024 px or less.
- One ad loads at most 22 images: the banner, the brand icon, and up to 20 carousel images.
- Decoded bitmaps share a 24 MiB budget. An image that doesn't fit is skipped.
- A download that exceeds
imageTimeoutMsfails.
A failed banner or brand icon fails the carousel. A carousel with no usable
image also fails. Failed images trigger AssetFailed, and the native banner can
take over.