Client Side Ad Insertion
The FLAdvertisingGoogleIMA library integrates with GoogleIMA to offer client-side ad insertion capability for both VOD and Live.
GoogleIMA
This section describes the supported SDK versions and how to set up GoogleIMA.
Supported GoogleIMA SDK versions:
- iOS 3.22.1
- tvOS 4.12.0
Create ad request
Create a GoogleIMAAdRequest with an ad tag URL. The following snippet shows
ad request creation with a sample ad URL endpoint:
let adTagUrl =
"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&" +
"iu=/124319096/external/ad_rule_samples&ciu_szs=300x250" +
"&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vast&" +
"cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&" +
"vid=short_onecue&correlator=&unviewed_position_start=1";
let adRequest = FLAdvertisingGoogleIMAFactory.adRequest(adTagUrl: adTagUrl);
Create ad player
Create an ad player using the main content player, GoogleIMAAdRequest, and
UIViewController. Note that you can't use the created ad player standalone
because it manages only GoogleIMA ad playback. The main content playback isn't
managed by this ad player. The view controller should be the parent of the
playback view.
// player is the main content player
let adPlayer = FLAdvertisingGoogleIMAFactory.adPlayer(
contentPlayer: player,
request: adRequest,
playerViewController: viewController,
shouldSupportPIP: true
);
Note: Prefetch license is required for picture-in-picture support. Otherwise, you'll see DRM failures.
When pre-warming pre-roll ads via QPAdsLoader.shared, pass enablePrefetchAds: true. For detailed integration steps, see Pre-roll ad prefetch.
Create ad composed player
AdComposablePlayer manages both the main content player and ad player,
providing seamless main content playback with GoogleIMA ads served per the
GoogleIMA ad response.
let adComposedPlayer = FLAdvertisingGoogleIMAFactory.adComposablePlayer(
contentPlayer: player,
adPlayer: adPlayer,
);
You can use this player like a conventional player from the FLPlayer library for playback.
Note: The GoogleIMA SDK provides the skip ad button if the ad is skippable. :::
Listening to Ad Events
The client can set delegate on ad player and listen to ad events to track the state of ad playback and implement custom UIs. The AdPlayer invokes the following ad delegate methods:
/// Called on ad playback failure
func adPlayer(ad: Ad?, didFailWith error: Error)
/// Called when initial and regular cue points are available (deprecated)
func adPlayerDidProvide(cuepoints: [Int])
/// Called when the unwatched cue points change
func adPlayerDidProvide(original cuepoints: [Int], unwatched cues: [Int])
/// Called with ad-incoming countdown updates
func adIncoming(with label: AdIncomingType)
/// Called on ad break start
func adPlayerDidStart(adBreak: AdBreak)
/// Called on ad start
func adPlayerDidStart(ad: Ad)
/// Called on ad playback progress change
func adPlayer(ad: Ad?, didChange progress: TimeInterval)
/// Called on ad playback buffer state change
func adPlayer(ad: Ad?, didChangeBuffer isBuffering: Bool)
/// Called on occurence of ad track event
func adPlayer(ad: Ad?, didTrack event: AdTrackingEvent)
/// Called on ad end
func adPlayerDidEnd(ad: Ad)
/// Called on ad break end
func adPlayerDidEnd(adBreak: AdBreak)
Ad-incoming notifications and cue points
For the ad-incoming countdown and the unwatched cue-point callbacks, see Incoming ad notification.
Ad Metadata
AdBreak
Represents an ad break object.
| Property | Type | Description |
|---|---|---|
| position | AdPosition | The position of the ad break, can be on of preroll, midroll or postroll |
| startTime | TimeInterval | The start position in seconds |
| duration | TimeInterval | The duration of the ad break, in seconds |
| totalAds | Int | Total number of adverts in the ad break |
The totalAds read from AdPlayer may not be accurate when it comes from adPlayerDidStart event. This may change once ad playback starts. GoogleIMA advises to read totalAds on or after FIRST_QUARTILE ad tracking event, to get a reliable data.
Ad
Represents an advert object.
| Property | Type | Description |
|---|---|---|
| adId | String | The identifier of the advert |
| sequence | Int | The sequence of the advert |
| duration | TimeInterval | The duration of the advert in seconds |
| isSkippable | Bool | Indicates if the ad is skippable |
| adBreak | AdBreak | The ad break which the advert belongs to |
| title | String | The title of the ad |
| vastProperties | VASTProperties | VAST properties of advert |
VASTProperties
Represents a VAST properties object.
| Property | Type | Description |
|---|---|---|
| skipOffset | TimeInterval | The number of seconds before the skip button is presented |
| isAutoPlay | Bool | Whether the player will auto-play content |
| adPosition | AdPosition | The position of the ad break, can be on of preroll, midroll or postroll |
| adSystem | string | The source ad server of the advert |
| AdTitle | string | The common name of the advert |
| adServingId | string | An identifier used to compare impression-level data across systems |
| creativeId | string | CreativeId of the advert |
| category | string | Category of the advert content |
| description | string | The longer description of the advert |
| advertiser | string | Advertiser name |
| pricing | string | Pricing element of the advert |
| survey | string | An URI to any resource file having to do with collecting survey data |
| expires | string | Number of seconds in which the ad is valid for execution |
Events tracked as part of AdTrackingEvent
| Event | Description |
|---|---|
| adBreakStart | Fired when an ad break in a stream starts. |
| adStart | Fired when an ad starts playing. |
| adFirstQuartile | Fired when the ad playhead crosses first quartile. |
| adMidpoint | Fired when the ad playhead crosses midpoint. |
| adThirdQuartile | Fired when the ad playhead crosses third quartile. |
| adPause | Fired when an ad is paused. |
| adResume | Fired when an ad is resumed. |
| adMute | Fired when an ad is muted. |
| adUnmute | Fired when an ad is unmuted. |
| adEnd | Fired when an ad completes playing. |
| adBreakEnd | Fired when an ad break in a stream ends. |
| adClicked | Fired when an ad is clicked. |
| adPlaybackViewTapped | Fired when a non-clickthrough portion of a video ad is clicked. |
| adIconTapped | Fired when ad icon is tapped. |
| adIconFallbackImageClosed | Fired on closing the icon fallback image dialog. This event only fires for Connected TV devices. |
| adSkipped | Fired when an ad was skipped. |
| adCompletedAll | Fired when all the valid ads in the ads response finished playing, or when the response doesn't return any valid ads. |
| adCuePointChanged | Fired when VOD stream cuepoints have changed. |
Picture in Picture
In order to support Picture in Picture, GoogleIMA SDK uses the main content player to play ad assets. In the process of playing ads, the main content asset and ad assets are switched with the player. Whenever a fairplay protected asset is loaded with player on ad break end, a on demand request for license key is made by the player. So it is suggested to prefetch the license key to avoid player requesting on-demand license whever a ad playback ends and main content is resumed playback.
CCPA compliance
The CCPA consent string must be sent as us_privacy query param with ad request url. Please check Google IMA SDK Documentation² to read further on for CCPA compliance.
The below code shows an ad request url with the IAB parameter "1YNN".
let adTagUrl =
'https://pubads.g.doubleclick.net/gampad/ads' +
'iu=/124319096/external/single_ad_samples&output=vast&us_privacy=1YNN';
Best Practices for Using AVPlayerViewController in tvOS
Overview
When integrating AVPlayerViewController in a tvOS application, it is recommended to use it as a child view controller within your playback view controller. This ensures that ads, ad UI, and focus management function correctly.
Implementation Example
Below is an example of how to properly integrate AVPlayerViewController within a custom playback view controller.
import UIKit
import AVKit
import FLPlayer
import FLPlayerInterface
class QPAVPlayerViewController: UIViewController {
// AVPlayerViewController instance for handling playback
private var playerViewController: AVPlayerViewController = AVPlayerViewController()
// Quickplay Player instance (set after the ViewController is created)
var qpPlayer: Player? {
didSet {
if let qpPlayer = qpPlayer, let avPlayer = qpPlayer.rawPlayer as? AVPlayer {
embedAVPlayerViewController()
playerViewController.player = avPlayer
addPlaybackView()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Ensure a smooth playback transition
}
// Embeds AVPlayerViewController within the current ViewController
private func embedAVPlayerViewController() {
addChild(playerViewController)
playerViewController.view.frame = view.bounds
view.insertSubview(playerViewController.view, at: 0)
playerViewController.didMove(toParent: self)
}
// Attaches the player’s playback view to AVPlayerViewController
private func addPlaybackView() {
guard let playbackView = qpPlayer?.playbackView else { return }
playbackView.frame = playerViewController.view.bounds
playerViewController.view.insertSubview(playbackView, at: 0)
}
}
Key Considerations
- Encapsulation:
AVPlayerViewControllershould always be contained within your playback view controller to properly handle media playback and interactions. - Focus & UI Handling: Using
AVPlayerViewControllerensures seamless integration of ads, player controls, and focus management on tvOS. - Full-Screen Playback: The example assumes that playback occurs in full screen, which is the default behavior on tvOS.
- Efficient View Management: The
playerViewControllerinstance is properly added and removed usingaddChild(_:)anddidMove(toParent:)to ensure memory management best practices.
By following these best practices, you can ensure a robust and scalable media playback experience on tvOS.
Ad Frequency Control (AFC)
To prevent ad fatigue by suppressing repeated ad breaks based on content consumption and exposure thresholds, refer to the Ad Frequency Control documentation.
Pre-roll ad prefetch
Pre-roll ad prefetch lets you request and warm up a Google IMA pre-roll ad break before the user starts playback. When the user selects a video, the player immediately uses the prefetched pre-roll ad, which eliminates initial ad buffering and network latency.
Prefetching operates via QPAdsLoader.shared in FLAdvertisingGoogleIMA,
warming up the IMAAdsManager in advance so the ad break is ready to play
instantly.
Integration & usage guide
1. Initializing ad loader & prefetching
Call initAdsLoader(adRequest:) early in your application or stream setup
sequence (such as during app launch or catalog display) to kick off prefetching:
import FLAdvertisingGoogleIMA
let adRequest = FLAdvertisingGoogleIMAFactory.adRequest(adTagUrl: "https://pubads.g.doubleclick.net/gampad/ads?...")
QPAdsLoader.shared.initAdsLoader(adRequest: adRequest)
Ad request options for prefetching
QPAdsLoader.shared.initAdsLoader(adRequest:) takes a GoogleIMAAdRequest configured with the pre-roll ad tag:
| Parameter | Type | Required | Description |
|---|---|---|---|
adTagUrl | String | Yes | The pre-roll VAST or VMAP ad tag endpoint URL. |
loadMediaTimeout | TimeInterval? | No | Timeout for loading the video ad media file (defaults to 8 seconds). |
adOverlayUIScope | [AdOverlayUIScope]? | No | Views to be treated as friendly obstructions by Google IMA. |
imaUiElementOption | IMAUiElementOption | No | Controls visibility of ad UI elements (countdown timer, attribution). |
playAdsAfterTime | TimeInterval? | No | Offset in seconds to resume and only play ads scheduled after that time. |
2. Creating composable player via factory
Use FLAdvertisingGoogleIMAFactory to construct GoogleIMAAdPlayer with
enablePrefetchAds (set to true to consume prefetched ads, or false to prevent consuming them) and wrap it with AdComposablePlayer:
Providing the enablePrefetchAds parameter (whether set to true or false) routes playback through the new AdvanceAdComposablePlayer (advanceCsaiPlayer) flow instead of the legacy player flow.
import FLPlayer
import FLAdvertisingGoogleIMA
// Create CSAI Ad Player enabling prefetched ads
let adPlayer = FLAdvertisingGoogleIMAFactory.adPlayer(
contentPlayer: mainContentPlayer,
request: adRequest,
shouldSupportPIP: true,
enablePrefetchAds: true,
playerViewControllerCallback: { presentingVC }
)
// Construct AdvanceAdComposablePlayer
let composablePlayer = FLAdvertisingGoogleIMAFactory.adComposablePlayer(
contentPlayer: mainContentPlayer,
adPlayer: adPlayer
)
// Begin playback
composablePlayer.play()
- Providing
enablePrefetchAds(trueorfalse) engages the newAdvanceAdComposablePlayerflow. - Prefetching is handled at the app level. It can happen regardless of the
enablePrefetchAdssetting. - Consumption is controlled by
enablePrefetchAds. It decides whether playback uses an already-prefetched pre-roll ad.
enablePrefetchAds | Prefetched ad available | Behavior |
|---|---|---|
true | Yes | Playback consumes the prefetched pre-roll ad. |
true | No | Playback follows the normal ad request flow. |
false | Yes | The prefetched ad is not consumed, even though it was already prefetched. |
false | No | Playback follows the normal ad request flow. |
Setting enablePrefetchAds to false does not stop the app from prefetching ads.
It only prevents playback from using them.
3. Playback lifecycle & event handling
AdvanceAdComposablePlayer handles switching between ads and main content
automatically:
// Observe player state changes
composablePlayer.addStateChangeBlock { player in
print("Current Player State: \(player.playbackState.value)")
}
// Pause or resume composable player
composablePlayer.pause()
composablePlayer.play()
When content finishes playing, inform the ads loader:
@objc func contentDidFinishPlaying(notification: NSNotification) {
QPAdsLoader.shared.adsLoader?.contentComplete()
}
4. Deinitializing ads loader
Call QPAdsLoader.shared.deinitAdsLoader() to reset the shared ads loader during user profile changes or login / logout:
// Stop composable player
composablePlayer.stop()
// Deinitialize shared loader and clear prefetch manager
QPAdsLoader.shared.deinitAdsLoader()
QPAdsLoader.shared.deinitAdsLoader() is used in the following scenarios:
- Login / Logout flow: Call
deinitAdsLoader()when the user logs in or logs out. - User profile change: When switching profiles, call
deinitAdsLoader()before the profile change, and then callinitAdsLoader(adRequest:)with the new profile's ad tag URL so the player does not reuse the previous profile's prefetched ad break.