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, or a 320x50 image
banner. The @quickplay/player library provides createCompanionPresentation
to show these companions. Your app supplies the container element, decides what
happens when a viewer taps a companion, and receives events for analytics.
Supported companion types
| Companion type | Supported | Ad delivery | Rendered by | Notes |
|---|---|---|---|---|
| Custom CTA | Yes | Any provider, through trafficking parameters | SDK | Icon, title, optional description, and one button. Never retained after the ad. |
| Standard (GPT) | Yes | Google IMA with VAST companions | IMA, in SDK-owned GPT slots | Requires the GPT script and an ad unit path. Default sizes are 320x50 and 300x250. |
| Standard (static image) | Yes | Datazoom MediaTailor SSAI, or any provider that fills AdInfo.companionAds | SDK | Image resources only. Your app dispatches the tracking URLs. |
| Carousel | No | Not applicable | Not applicable | The SDK doesn't render carousels. Your app can render its own, and a standard banner can act as a fallback. |
| HTML, iframe, or takeover | No | Not applicable | Your app | If ad_type=webview, the SDK shows nothing so your app can use the area. |
For the Android equivalent, see Android companion ads.
Companion ads are available for web browsers only. They are not supported on Smart TV platforms, including WebMAF and PS5.
Choose a companion format
The presentation shows one companion at a time. Each ad can use one of three flavours, depending on how the ad is delivered.
| Flavour | When to use it | Where the creative comes from | Who renders it | Who tracks it |
|---|---|---|---|---|
| Custom CTA | You want a branded banner with an icon, copy, and a button. | Ad trafficking parameters | The SDK | Your app, through onEvent |
| Standard (GPT) | You serve ads with Google IMA and have VAST companions. | IMA VAST companions | IMA, into SDK-owned Google Publisher Tag (GPT) slots | IMA |
| Standard (static) | You use Datazoom MediaTailor SSAI and its ads include static image companions. | AdInfo.companionAds | The SDK, as a static image | Your app, through TrackingRequested |
When more than one flavour is available for an ad, the player picks one using these rules:
- A custom CTA takes precedence while its icon loads and while it is shown.
- If the CTA can't render, a standard banner can take its place.
- A ready static banner takes precedence over a GPT banner.
- Static banners support image resources only. They don't load HTML or iframes.
Your app, not the SDK, is responsible for:
- Page layout, orientation, and picture-in-picture (PiP) behavior.
- Deep links and operations such as watchlist or reminders.
- Takeover rendering. If a trafficking parameter sets
ad_type=webview, the SDK shows no companion so that your app can use the area. - Carousel, contextual ads, and advertiser tabs. The SDK doesn't render them. A carousel ad still permits a standard banner as a fallback.
Before you begin
-
Install
@quickplay/player. -
For GPT companions, load the GPT script on your page. The SDK registers its slots through
googletag.cmd, so GPT can still be loading when you create the presentation.<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script> -
Static companions don't need GPT or an ad unit path.
The SDK registers its slots with the companion service only, never with
PubAdsService. They can't request regular display ads, and your other GPT slots
aren't affected.
Create the companion presentation
Create the presentation before the first adstart event. Keep one presentation
for the lifetime of the IMA display container, and release it when that
container is destroyed.
import { createCompanionPresentation } from '@quickplay/player';
const companion = createCompanionPresentation({
container: document.getElementById('companion')!,
adPlayer,
configuration: {
native: { adUnitPath: '/1234/companion' }, // GPT companions only
nativeSizes: [{ width: 320, height: 50 }],
persistCompanionAfterAd: true,
persistStaticCompanionAfterAd: true,
},
actions: {
onAction: async ({ url }, updateState) => {
updateState({ label: 'Opening…', disabled: true });
await router.open(url);
updateState({ label: 'Opened', disabled: false });
return true;
},
},
onEvent: event => {
if (event.type === 'TrackingRequested') {
// Dispatch these URLs through your app's approved tracking client.
event.urls.forEach(url => companionTracking.send(url));
}
analytics.track(event);
},
});
| Property | Required | Description |
|---|---|---|
container | Yes | The element the SDK draws the companion into. |
adPlayer | Yes | The ad player to follow. |
configuration | No | Formats, sizes, retention, and theme. See Configuration reference. |
actions.onAction | No | Handles taps on a companion. See Handle taps. |
onEvent | No | Receives presentation events. See Listen to events. |
Manage the presentation
Use these methods across content changes and for teardown:
companion.setVisible(false); // Hide while PiP or another ad UI is showing
companion.setVisible(true); // Restore the same creative and slots
companion.setAdPlayer(nextAdPlayer); // Clear, then follow the new player
companion.setAdPlayer(undefined); // Clear and detach when playback stops
companion.release(); // Final teardown
When the content changes, call setAdPlayer(nextPlayer) on the existing
presentation instead of creating a new one. The IMA loader keeps its GPT slot
references across plays. Destroying and recreating the slots can stop later
companions from rendering.
Set up a custom CTA
A custom CTA is a banner with an icon, a title, optional description, and one
button. The SDK builds it from the ad's trafficking parameters, which are a
comma-separated list of key=value pairs.
ad_type=custom_cta,icon=https://cdn.example.com/brand.png,bannerTitle=Watch now,bannerDescription=Start the next episode,ctaText=Watch,ctaUrl=https://www.example.com/watch,campaign=spring,bgColour=#1A1A1A,bannerTextColor=#FFFFFF,ctaBgColor=#E53935,ctaTextColor=#FFFFFF
| Parameter | Required | Description |
|---|---|---|
ad_type | No | custom_cta or custom. Omit it to let the SDK detect a CTA from its fields. |
icon | Yes | HTTP(S) URL of the icon image. |
bannerTitle | Yes | Title text. It can't be blank. |
ctaUrl | Yes | Destination URL or an app-routed deep link. |
bannerDescription | No | Additional copy. |
ctaText | No | Button label. Defaults to Learn more. |
bgColour, bannerTextColor | No | Banner background and text colors. |
ctaBgColor, cta_bg_color, ctaColour | No | Button background color. |
ctaTextColor, cta_text_color | No | Button text color. |
campaign, campaign name | No | Campaign name included in presentation events. |
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.
icon,bannerTitle, andctaUrlmust all be present. Unrelated parameters alone don't create a CTA.- If you supply more than one button background alias, the values must match.
- Split on the first
=only, so URL query strings are preserved. There is no way to escape a literal comma. - The SDK rejects the creative if it finds duplicate keys, conflicting aliases, malformed segments, or an unsupported schema version.
- An explicit
ad_typeother thancustom_ctaorcustomis never treated as a CTA.
Colors
Colors resolve in this order: the creative, then configuration.theme, then the
built-in defaults. Use #RRGGBB or opaque #FFRRGGBB values. The SDK ignores
invalid or translucent colors. If text contrast is below 4.5:1, the SDK falls
back to black or white. Copy and button labels are always set as text, never as
HTML.
Use your own decoder
If your ads use a different parameter format, set configuration.decode. The
decoder is synchronous and must not throw on malformed ad data. Return a
CompanionDecodeResult with one of the following:
creativeto show a custom CTA.reasonto reject the creative.suppressStandard: truewhen another app presentation owns the area.
Set up standard companions
GPT companions with Google IMA
IMA renders standard VAST companions into GPT slots that the SDK owns. To enable them:
- Load the GPT script on your page.
- Set
configuration.native.adUnitPathto your companion ad unit. - Set
configuration.nativeSizesto the sizes you want to accept. The default is 320x50 and 300x250.
IMA owns rendering, clicks, and beacons for GPT companions, so you don't dispatch any tracking URLs for them.
If GPT was requested but googletag isn't available, the presentation emits a
CapabilityLimited event.
Static companions with Datazoom MediaTailor
For Datazoom MediaTailor SSAI, the player exposes the ad's companions as
AdInfo.companionAds. Other providers can populate the same optional model.
Each CompanionAd has the following fields:
| Field | Required | Description |
|---|---|---|
width | Yes | Declared width of the image. |
height | Yes | Declared height of the image. |
staticResource | Yes | Image URL. |
id | No | Companion identifier. |
clickThroughUrl | No | Destination when the viewer taps the banner. |
impressionTrackingUrls | No | Impression URLs your app dispatches. |
clickTrackingUrls | No | Click URLs your app dispatches. |
The SDK picks the largest nativeSizes entry that fits the container and has
an HTTP(S) image resource. If nothing matches, no static companion shows. The
image must load within imageTimeoutMs. If the container is resized during the
ad, the SDK can select a different size. A retained banner that no longer fits
stays hidden until the container is large enough again.
Taps on a static banner go through the same onAction callback as a custom
CTA, so your app can pause playback before opening the destination.
Dispatch tracking URLs
For static companions, the SDK doesn't send tracking pixels. Instead, it emits
TrackingRequested events, and your app must send each URL exactly once.
onEvent: event => {
if (event.type === 'TrackingRequested') {
// event.tracking is 'impression' or 'click'
event.urls.forEach(url => companionTracking.send(url));
}
},
- An impression request is emitted once per displayed static companion per ad, and only after the image has loaded and is visible.
- A click request is emitted once per accepted tap. Repeated taps are suppressed while an action is pending and for 500 ms after it settles.
- URLs are deduplicated and limited to HTTP(S).
TrackingRequestedasks your app to send the URLs. It doesn't confirm that a network request succeeded.
Handle taps
The SDK calls onAction when a viewer taps a custom CTA or a static banner.
actions: {
onAction: async ({ url, campaign, context }, updateState) => {
updateState({ label: 'Adding…', disabled: true });
await myList.add(url);
updateState({ label: 'Added', disabled: false });
return true;
},
},
onAction receives the url, the campaign, an optional ad context, and an
updateState callback scoped to the current ad.
| Return value | Result |
|---|---|
true | Your app handled the action. |
false | The SDK opens the URL in a new tab with _blank and noopener,noreferrer. The default opener allows HTTP(S) URLs only. |
To change the button, call updateState({ label, disabled, hidden }):
labelchanges the button text.disableddisables the button.hidden: trueremoves the CTA and lets a standard banner take its place.
Keep these behaviors in mind:
- The button is disabled while an action runs and for a 500 ms cooldown after.
- State resets on every new ad. To set an initial state, call
setActionStateduring the active ad. - Async handlers must use the
updateStatecallback they receive. Updates from a settled, timed-out, or earlier ad's action are ignored. - If the handler throws or rejects, the outcome is
failed. - If the handler runs longer than
actionTimeoutMs, the outcome istimedOut, and the SDK never falls back to the default opener later. - If the handler completes while the presentation is hidden, the default
opener doesn't run, and the outcome is
blocked.
Control visibility and lifecycle
setVisible(false) hides the SDK-owned companion and blocks taps. It keeps the
current creative, action state, and GPT slots. Asset loading and ad events keep
running while hidden. When you show it again, the SDK restores a valid creative
without sending a second static impression. Your app owns the PiP, fullscreen,
orientation, and competing-presentation rules.
| Signal | Behavior |
|---|---|
adstart | Clears the previous creative and action state, then decodes and loads the new ad. |
adend | Removes the CTA. Keeps an already displayed banner only if retention is on for its source. |
adbreakend | Ends an active ad if adend was missing, then reports retention. |
adbreakstart | Drops a banner retained from the previous break. |
adskip, IMA skip tracking, aderror, clear() | Clears everything, regardless of retention. |
setAdPlayer | Clears and rebinds to the new player without replacing GPT slots. |
release() | Unsubscribes and removes SDK views, listeners, and GPT slots. |
Keep a banner after the ad ends
By default, a companion disappears when its ad ends. To keep a displayed
banner on screen until the next break, set the retention options. Both default
to false.
| Option | Applies to |
|---|---|
persistCompanionAfterAd | GPT banners |
persistStaticCompanionAfterAd | Static SSAI banners |
A retained banner stays until the next ad or break, an explicit clear, an error, a skip, or teardown. Keep in mind:
- A custom CTA is never retained.
- An image that finishes loading after the ad has ended isn't retained.
- A
Retainedevent fires once after content resumes while the banner is visible. - GPT fills that arrive between ads in an active break stay hidden until the
next
adstart.
Listen to events
onEvent receives the following events:
| Event | When it fires |
|---|---|
DisplayDecision | The SDK chose what to show. It includes the format (or none with a reason), the size, and the campaign. |
CapabilityLimited | GPT was requested but googletag is unavailable. |
AssetFailed | A CTA icon or static companion image failed to load or timed out. |
Retained | A retained standard banner is still visible after the break. |
ActionResult | A tap resulted in opened, handled, blocked, failed, or timedOut. |
TrackingRequested | Static companion impression or click URLs your app must dispatch. |
Events and actions include an optional context with adId, creativeId,
adBreakId, adSessionId, and delivery, when the provider supplies them. An
exception thrown in your listener doesn't break the presentation.
Configuration reference
All configuration fields are optional.
| Option | Default | Description |
|---|---|---|
formats | ['customCta', 'standard'] | Formats that can be shown. |
decode | Built-in decoder | Your own trafficking parameter decoder. |
native.adUnitPath | None | GPT ad unit path. Required for GPT companions. |
native.googletag | window.googletag | The GPT global. |
nativeSizes | 320x50 and 300x250 | GPT slot sizes and the static sizes the SDK accepts. |
persistCompanionAfterAd | false | Keep a displayed GPT banner after its ad. |
persistStaticCompanionAfterAd | false | Keep a displayed static banner after its ad. |
imageTimeoutMs | 8000 | Time allowed for an image to load. |
actionTimeoutMs | 5000 | Time allowed for onAction to settle. |
theme | Built-in colors | surfaceColor, textColor, buttonColor, and buttonTextColor. |