Skip to main content

Companion ads

Companion ads display standard, carousel, or custom call-to-action creatives alongside ads played through FLPlayer. You provide a CompanionContainerView; the presentation manages its height and the content inside it. 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 the equivalent platform guides, see Android companion ads and Web companion ads. The APIs and some provider capabilities differ by platform.

Schedule contextual ads​

Use the playhead boundary observer APIs to coordinate contextual ad rendering at selected stream-time positions. The observer reports timing only; your app parses the contextual ad data and renders the ad. This integration is separate from CompanionPresentation. Register an observer for each playback with the positions you want to observe.

let observerToken = player.addPlayheadBoundaryObserver(
positions: [30, 60],
leadTimeMs: 1_000
) { position in
// Coordinate rendering the contextual ad for this stream-time position.
}

leadTimeMs sets how long before each position the callback fires. The callback receives the boundary position. Retain the returned token for the current playback and remove the observer when playback completes or the user stops playback:

if let observerToken = observerToken {
player.removePlayheadBoundaryObserver(token: observerToken)
}

Registration can return nil; handle the case where no observer token is available.

Implement takeover ads in the application​

Takeover ads are not a CompanionPresentation format. The app must interpret 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 the ad's traffickingParametersParse the takeover fields, then show and dismiss the takeover with the ad lifecycle.
ContextualPlayhead boundaries and contextual ad metadataParse the contextual ad 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 CompanionPresentation 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 takeoverAdsUrl in the iOS model.

For example, the in-video VAST parameters may look like this:

ad_type=webview,campaign=<campaign-name>,Image_url=<https-url>

The design identifies ad_type=webview as an Android convention and notes that iOS handling isn't confirmed. Verify that this format applies to your iOS app and ad setup; it isn't a built-in iOS SDK format.

For in-video takeovers, verify that the ad model used by your integration exposes the raw traffickingParameters. The playhead boundary observer provides timing only; it doesn't provide or parse takeover metadata. If the ad model doesn't expose this value, it must be made available to the app as optional ad metadata; takeover-specific parsing still belongs in the app.

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 formats​

The presentation displays one companion format at a time. It supports SDK-rendered carousel and custom CTA creatives, plus a native IMA slot when you configure a fixed standardSlotSize.

FormatCreative sourceRenderingBehavior
CarouselAd trafficking parametersiOS SDKPortrait-only; can remain visible after the ad when retention is enabled.
Custom CTAAd trafficking parametersiOS SDKDisplays an icon, title, optional description, and action button.
Standard native slotIMA companion responseGoogle IMA in an SDK-owned viewFixed size per presentation; unavailable on tvOS.

Use the native slot as a fallback for ads without an eligible in-house creative when the .standard format is enabled and a fixed size is configured. If a required carousel or CTA image fails to load, the presentation also falls back to the native slot when those settings are in place. Carousels switch to the native slot in landscape when it is available.

Set up a companion presentation​

Add the container to your view hierarchy, constrain its position, and set its width through prefferedWidth. Don't add a separate width or height constraint: the container applies its width internally, and the presentation updates its height to fit the active creative.

import FLPlayer

let companionView = CompanionContainerView()
// Add companionView to your view hierarchy and constrain its position.
// Set this to the width available in your layout.
companionView.prefferedWidth = availableWidth

let companionSize = UIDevice.current.userInterfaceIdiom == .pad
? CGSize(width: 728, height: 90)
: CGSize(width: 320, height: 50)

let companionPresenter = CompanionPresentation(
containerView: companionView,
standardSlotSize: companionSize,
configuration: CompanionConfiguration(
formats: [.standard, .carousel, .customCTA]
),
actions: companionActions
)

companionPresenter.onReceiveEvents { event in
print("Companion event: \(event.kind)")
}

standardSlotSize is optional and sets the fixed size for the native IMA companion slot. Use these standard banner sizes as a starting point:

DeviceStandard slot size
iPhone320 × 50 points
iPad728 × 90 points

The 320 × 50 banner is a standard size for phones and tablets; 728 × 90 is a standard tablet leaderboard size. Use the iPad size only when the ad inventory supports it and the available layout can fit it. For more information, see Google's fixed-size banner ad sizes.

The slot size applies to the native IMA fallback slot, not to every creative rendered by CompanionPresentation. Set prefferedWidth to the width available to the container and update it when the layout changes, including after device rotation. The presentation manages the container height. Pass nil for standardSlotSize when you don't need a fixed native IMA slot.

Supply creative parameters​

The SDK reads in-house creatives from comma-separated key=value parameters. Use the parameter names and capitalization shown below. The parser splits each segment at its first =, so URL query strings are preserved. A malformed segment or duplicate key rejects the creative.

Custom CTA​

A custom CTA requires an icon, title, and action URL. If the ad doesn't specify ctaText, the SDK displays “Learn more.”

ParameterRequiredDescription
icon or IconYesHTTP or HTTPS URL for the icon. If both aliases are present, their values must match.
bannerTitleYesNon-empty title.
ctaUrlYesDestination URL.
bannerDescriptionNoDescription shown below the title.
ctaTextNoAction button label. Defaults to Learn more.
campaign or campaign nameNoCampaign name included in presentation events. If both aliases are present, their values must match.
companion_schema_versionNoSchema version. The only supported value is 1.

If you don't provide a CompanionActions handler, the SDK opens HTTP and HTTPS destinations with UIApplication. Provide a handler to process app-specific actions or other URL schemes.

Set ad_type=carousel to identify a carousel. Required image and title parameters must be valid; the description key is required but its value can be empty.

ParameterRequiredDescription
carousel_bannerYesHTTP or HTTPS URL for the collapsed banner.
brand_iconYesHTTP or HTTPS URL for the expanded header icon.
carousel_titleYesNon-empty title.
carousel_descriptionYesDescription; an empty value is allowed.
carousel_imagesYes$-separated HTTP or HTTPS image URLs. Include at least one usable image and no more than 20 entries.
carousel_images_urlsNo$-separated tap destinations matched to images by position.
carousel_cta_text and carousel_cta_urlOptional pairShared action button label and HTTP or HTTPS destination. Provide both or neither.
campaign or campaign nameNoCampaign name included in presentation events. If both aliases are present, their values must match.
companion_schema_versionNoSchema version. The only supported value is 1.

Invalid image URLs are skipped. A blank entry in carousel_images_urls leaves the corresponding image without a tap destination.

Connect the presenter to an ad player​

Pass the presenter to the ad integration that delivers the companion ads. The same presentation API supports Google IMA and MediaTailor.

Google IMA​

Pass companionPresenter to FLAdvertisingGoogleIMAFactory.adPlayer. If you prefetch ads, create a separate presenter and container for QPAdsLoader.

let prefetchPresenter = CompanionPresentation(
containerView: CompanionContainerView(),
standardSlotSize: companionSize
)
QPAdsLoader.shared.initAdsLoader(
adRequest: adRequest,
companionPresenter: prefetchPresenter
)

let adPlayer = FLAdvertisingGoogleIMAFactory.adPlayer(
contentPlayer: player,
request: adRequest,
enablePrefetchAds: true,
companionPresenter: companionPresenter,
playerViewControllerCallback: { viewController }
)

The SDK prepares and adopts prefetched slots. You don't need to call prepareSlots(_:) or adoptSlots(_:) for this integration.

MediaTailor​

Pass the presenter to the MediaTailor player builder before calling build().

var builder = FLMediatailorFactory.createPlayerBuilder(
mediatailorSession: session,
fallbackAsset: fallbackAsset
)
builder.withCompanionPresenter(companionPresenter)
let adPlayer = builder.build()

Configure creatives and handle actions​

CompanionConfiguration controls which creative formats can display, whether a carousel remains visible after an ad ends, its theme, and the image loading timeout.

SettingDefaultDescription
formats.standard, .carousel, and .customCTACreative formats the presentation can render.
persistCarouselAfterAdfalseKeeps a carousel visible after the ad ends.
themeCompanionTheme()Sets optional surface, text, button, and button text colors.
imageTimeout10 secondsMaximum time to wait for a creative image.

presentationMode controls when carousel creatives are eligible. It defaults to .automatic, which uses the current orientation. Set it to .portrait, .landscape, or .fullscreen to force a mode. Changing the mode re-evaluates the active carousel.

Implement CompanionActions to handle creative click-through actions. Call the completion handler exactly once with the result.

import UIKit

final class AppCompanionActions: CompanionActions {
func execute(
_ action: CompanionAction,
completion: @escaping (CompanionActionResult) -> Void
) {
UIApplication.shared.open(action.url) { success in
completion(
success
? .completed
: .failed("Could not open \(action.url)")
)
}
}
}

Register onReceiveEvents(_:) to observe presentation, expansion, asset, and action results. The SDK delivers event callbacks on the main thread.

Events include displayDecision, capabilityLimited, cleared, retained, expansionChanged, itemChanged, assetFailed, and action request and outcome events.

Manage the presentation lifecycle​

The ad player manages the presentation lifecycle for Google IMA and MediaTailor. If you build a custom ad integration, call the lifecycle methods in this order:

MethodWhen to call
prepareSlots(_:)Before the ad request, to get native slot configuration for the IMA display container.
adoptSlots(_:)When adopting a native slot created for a prefetched request.
adDidStart(_:)When an ad starts.
adDidEnd(_:)When an ad ends.
contentDidResume()When content playback resumes after an ad break.
dispose()When the ad session ends. Cancels pending work and removes presentation-owned views.

All lifecycle methods are safe to call from any thread. Calls after dispose() are ignored. On tvOS, native IMA companion slots aren't available: prepareSlots(_:) returns an empty array and the presentation emits a capabilityLimited event.

After an ad ends and content resumes, the presentation clears the creative. When persistCarouselAfterAd is true, the carousel remains visible and the presentation emits a retained event.

Use the container in SwiftUI​

SwiftUI apps can host CompanionContainerView through a UIViewRepresentable wrapper. Implement sizeThatFits to read the presentation-managed height; this API requires iOS 16 or tvOS 16. Set the container width from the available layout width and update it when that width changes.

struct CompanionContainerRepresentable: UIViewRepresentable {
let width: CGFloat
let container: CompanionContainerView

func makeUIView(context: Context) -> CompanionContainerView {
container.setContentHuggingPriority(.defaultLow, for: .horizontal)
container.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return container
}

func updateUIView(_ uiView: CompanionContainerView, context: Context) {
DispatchQueue.main.async {
uiView.prefferedWidth = width
}
}

@available(iOS 16.0, tvOS 16.0, *)
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: CompanionContainerView,
context: Context
) -> CGSize? {
let height = uiView.preferredHeight.isFinite
? max(0, uiView.preferredHeight)
: 0
return CGSize(width: width, height: height)
}
}

In your SwiftUI view, pass the available width to the wrapper. Use a stable .id(...) based on the container identity if SwiftUI must recreate the wrapper when the container changes.