Skip to main content

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 typeApp inputApp responsibility
In-videoIMA ad events and VAST traffickingParametersParse the takeover fields, then show and dismiss the takeover with the ad lifecycle.
ContextualVOD cue list or live SCTE-35 signal and contextual ad metadataParse the contextual data and render it at the intended stream-time position.
Ads under a tabDetails-page ad dataParse 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 typeSourceKeys or fields
In-videoVAST traffickingParametersad_type=webview, campaign, and Image_url (the page URL despite its name).
ContextualGAM native custom formatad_type=takeoverAds and Takeover_ads_url.
Ads under a tab, GAMGAM native custom formatad_type=takeoverAds, Takeover_ads_url, redirection_url, sidelogo, c_title, thirdPartyImpression, thirdPartyClick, and advertiser_name.
Ads under a tab, backendDetails-page backend dataAdvertiserTab.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 typeSupportedAd deliveryRendered byNotes
CarouselYesAny provider, through trafficking parametersSDKPortrait only. Collapsed banner that expands into up to 20 images. Can be retained after the ad.
Custom CTAYesAny provider, through trafficking parametersSDKIcon, title, optional description, and one button. Never retained after the ad.
Native IMA bannerYesIMA CSAI with VAST companionsIMA, in an SDK-owned slotOne fixed size per presentation. Not available with IMA DAI or Datazoom MediaTailor.
Static image (SSAI)NoNot applicableNot applicableUse carousel or custom CTA creatives for SSAI ads.
HTML, iframe, or takeoverNoNot applicableYour appThe 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.

FormatWhen to use itWhere the creative comes fromWho renders itWho tracks it
CarouselYou want a collapsed brand banner that expands into a gallery of images.Ad trafficking parametersThe SDKYour app, through onEvent
Custom CTAYou want a branded row with an icon, copy, and a button.Ad trafficking parametersThe SDKYour app, through onEvent
Native IMA bannerYou serve ads with Google IMA and have VAST companions.IMA VAST companionsIMA, into an SDK-owned slotIMA

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 presentationMode to 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-player module 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 CompanionPresentation call must run on the main thread.
tip

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.

ParameterRequiredDescription
containerYesThe ViewGroup the SDK draws the companion into. Use a container whose height can wrap its content.
standardSlotSizeNoA CompanionSlotSize.Fixed(widthPx, heightPx) for the native IMA banner. Sizes are pixels. Omit it to show only the carousel and custom CTA.
configurationNoFormats, theme, retention, and timeouts. See Configuration reference.
actionsNoA CompanionActions callback that handles taps. See Handle taps.
onEventNoReceives CompanionEvent values. See Listen to events.
note

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
important

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
ParameterRequiredDescription
icon or IconYesHTTP(S) URL of the icon image. Without it the ad has no CTA, and no rejection reason is reported.
bannerTitleYesTitle text. It can't be blank.
ctaUrlYesDestination. Any scheme is accepted.
ctaTextNoButton label. Defaults to Learn more.
bannerDescriptionNoCopy below the title.
bgColourNoSurface color.
bannerTextColorNoTitle and copy color.
ctaBgColor, cta_bg_colorNoButton background color.
ctaTextColor, cta_text_colorNoButton text color.
campaign, campaign nameNoCampaign name included in event campaign fields.
companion_schema_versionNoOnly 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, icon and Icon with different values, or campaign and campaign name with 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 DisplayDecision event, 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.

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.

ParameterRequiredDescription
ad_typeYesMust be carousel.
carousel_bannerYesHTTP or HTTPS image URL for the collapsed banner.
brand_iconYesHTTP or HTTPS image URL for the expanded header.
carousel_titleYesNonblank text.
carousel_descriptionYesThe key must exist. The value may be empty.
carousel_imagesYes$-separated HTTP or HTTPS image URLs. At least one must be usable.
carousel_images_urlsNo$-separated tap targets, matched to the images by position.
carousel_cta_textNoLabel of the shared button. Needs carousel_cta_url.
carousel_cta_urlNoHTTP or HTTPS target of the shared button. Needs carousel_cta_text.
carousel_cta_bg_colorNoShared button background color.
carousel_cta_textColorNoShared button text color.
campaign, campaign nameNoCampaign name included in event campaign fields.
companion_schema_versionNoOnly 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_images entries. 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 ItemChanged and accessibility labels.
  • A blank entry in carousel_images_urls leaves that image inert.
  • The shared button needs both carousel_cta_text and carousel_cta_url. When the shared button exists, image taps are disabled.

Your app controls the carousel through these settings:

  • formats must contain CompanionFormat.CAROUSEL for the carousel to show.
  • persistCarouselAfterAd keeps a completed carousel on screen after the ad. See Keep a carousel after the ad ends.
  • theme supplies the surface and text colors. The carousel ignores bgColour.
note

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:

  1. Pass a fixed standardSlotSize when you create the presentation.
  2. Keep CompanionFormat.STANDARD in formats. It is included by default.
  3. 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.

note

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 valueResult
CompanionActionResult.CompletedYour app handled the action.
CompanionActionResult.UnsupportedYour 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 CompanionActions callback 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().

SignalBehavior
Ad startClears 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 errorClears everything, regardless of retention.
Content replaced, stop, or stream endClears 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.

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 Retained event fires when the carousel becomes retained.
  • Turning persistence off with updateConfiguration clears a retained carousel.

Listen to events​

onEvent receives a sealed CompanionEvent. Every event carries the visible format and the creative's optional campaign.

EventWhen it fires
DisplayDecisionAn ad starts. It reports the reason for the first decision.
CapabilityLimitedThe provider can't render native companions, for example with IMA DAI or Datazoom MediaTailor, while a managed standardSlotSize is set.
ClearedAn ad is skipped, errors, ends playback, or content changes.
RetainedA carousel becomes retained after the ad.
ExpansionChangedThe user expands or collapses the carousel.
VisibilityChangedThe visible format changes on screen.
ItemChangedThe centered carousel image changes.
AssetFailedA required image fails to load.
ActionRequested, ActionSucceeded, ActionFailedA 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.

OptionDefaultDescription
persistCarouselAfterAdfalseKeep a completed carousel on screen after the ad.
formatsSTANDARD, CAROUSEL, CUSTOM_CTAFormats the presentation may choose.
themeCompanionTheme()Fallback surface, text, and button colors.
actionTimeoutMs10000Time allowed for CompanionActions.execute to settle.
imageTimeoutMs10000Time allowed for an image download.

CompanionTheme takes optional color values for the fallback colors:

OptionDescription
surfaceColorBackground color of the creative.
textColorTitle and copy color.
buttonColorButton background color.
buttonTextColorButton 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 imageTimeoutMs fails.

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.