Server-Side Ad Insertion using FLAdvertisingBroadpeak
Server-side Ad Insertion (SSAI) player powered by Broadpeak SmartLib. SmartLib intercepts the content URL and rewrites it to a Broadpeak-hosted stream that seamlessly stitches ad breaks into the content, resulting in a broadcast-like viewing experience.
Prerequisites
| Requirement | Details |
|---|---|
| iOS | 14.0+ |
| tvOS | 14.0+ |
| Swift | 5.8+ |
| Xcode | 26.0+ |
| Broadpeak SmartLib | 6.0.2 (exact) |
| FLPlayer | Latest compatible release |
Your app must have an active Broadpeak account and a configured SmartLib CDN domain. Contact Broadpeak to obtain your CDN domain name(s).
Installation
Swift Package Manager
Add the following package dependencies to your Xcode project:
Broadpeak SmartLib (SPM — exact version 6.0.2):
https://delivery-platform.broadpeak.tv/ios/broadpeak/smartlib-package.git
Select the following products:
SmartLib-AVPlayer— AVPlayer integration (required)SmartLib-OMSDK— Open Measurement support (required for OM SDK, iOS only)
Firstlight SDK — add the FLAdvertisingBroadpeak module to your target.
Xcode Entitlements
No special entitlements are required beyond standard AVFoundation usage. If you plan to use PiP, ensure the Background Modes → Audio, AirPlay, and Picture in Picture capability is enabled.
SmartLib Initialization
SmartLib must be initialized once at application launch, before any player is created. The recommended place is AppDelegate.application(_:didFinishLaunchingWithOptions:).
Build Initialization Data
import FLAdvertisingBroadpeak
let initData = FLAdvertisingBroadpeakFactor.initializationData(
broadpeakDomainNames: .Custom(["stream.example-cdn.broadpeak.io"]),
analyticsAddress: .Custom(["https://analytics.example.com"]),
allowStorage: consentGranted, // optional — set true after obtaining user consent
uuid: UUID(), // optional — persistent user identifier
deviceType: nil, // optional — override device type string
userAgent: nil, // optional — custom analytics user agent
gdprPreference: .GDPR_CLEAR, // optional — default: .GDPR_CLEAR
sessionKeepLiveFrequency: nil, // optional — ms, range 5000–10000
loggerLevel: .levelNone // optional — use .verbose for debugging
)
broadpeakDomainNames controls which URLs SmartLib intercepts:
| Value | Behaviour |
|---|---|
| .All | All session URLs are treated as Broadpeak CDN streams |
| .None | No URLs are intercepted (effectively disables SSAI) |
| .Custom(["cdn.example.com"]) | Only URLs matching these hostnames are intercepted |
analyticsAddress:
| Value | Behaviour |
|---|---|
| .None | Analytics disabled |
| .Custom(["https://analytics.host"]) | Send Broadpeak analytics to these servers |
allowStorage controls Google PAL TCFv2 consent — whether the PAL SDK may use local storage. Always defaults to false; only set true after obtaining the appropriate user consent (GDPR/CCPA). You can override this per-session via PALSessionConfiguration.allowStorage.
Initialize the Session Manager
FLAdvertisingBroadpeakFactor.broadpeakSessionManager.createInstance(initializationData: initData)
createBroadpeakPlayer will report a SMARTLIB_INSTANCE_NOT_INITIALIZED error via adPlayer(ad:didFailWith:) if this step is skipped.
Create a Broadpeak Player
Build the Session Data
Session data configures each individual playback session. Create a new instance per playback.
let sessionData = FLAdvertisingBroadpeakFactor.broadpeakSessionData(
pipSession: false, // true only for a PiP-spawned secondary session
customParams: ["env": "production"], // optional — sent to Broadpeak analytics
adParams: ["targeting_key": "value"], // optional — appended to ad requests
palConfiguration: palConfiguration // optional — see Google PAL Integration
)
| Property | Type | Required | Description |
|---|---|---|---|
| pipSession | Bool? | No | Set true when spawning a second session for Picture-in-Picture. |
| customParams | [String: String]? | No | Custom key-value pairs forwarded to Broadpeak analytics. |
| adParams | [String: String]? | No | Custom parameters appended to ad request URLs. |
| palConfiguration | PALSessionConfiguration | No | Google PAL nonce configuration. See Google PAL Integration. |
Create the Player
let player = FLAdvertisingBroadpeakFactor.createBroadpeakPlayer(
initialisationUrl: "https://stream.cdn.example.com/live/index.m3u8",
fallbackURL: URL(string: "https://origin.cdn.example.com/live/index.m3u8")!,
sessionData: sessionData,
adPlaybackPolicy: adPlaybackPolicy, // optional — see Ad Playback Policy
thumbnailConfiguration: nil
)
player.addDelegate(self) // add before play() so no events are missed
| Parameter | Type | Required | Description |
|---|---|---|---|
| initialisationUrl | String | Yes | The Broadpeak SSAI stream URL. SmartLib resolves this asynchronously to the ad-stitched playback URL. |
| fallbackURL | URL | Yes | Direct origin URL used as the player asset while SmartLib resolves the SSAI URL, and as a fallback on resolution failure. |
| sessionData | BroadpeakSessionData | Yes | Per-session Broadpeak configuration. |
| adPlaybackPolicy | AdPlaybackPolicy? | No | Ad behaviour rules for seek, interrupt, etc. |
| thumbnailConfiguration | ContentThumbnailPreviewConfiguration? | No | Thumbnail preview configuration. |
createBroadpeakPlayer always returns a non-optional player immediately. Any initialisation error (SmartLib not initialised, invalid URL, or SmartLib URL resolution failure) is reported asynchronously via adPlayer(ad:didFailWith:) as soon as a delegate is added.
Player Lifecycle Management
BroadpeakPlayer conforms to AdPlayer, which conforms to Player. Use it wherever a standard Player is accepted.
Attach the Player to a View
playerContainerView.addSubview(player.playbackView)
player.playbackView.frame = playerContainerView.bounds
Playback Control
player.play()
player.pause()
player.seek(to: 120.0) // seek to 120 seconds
player.stop()
seek(to:) during an ad break is subject to the configured AdPlaybackPolicy. The policy may redirect the seek to a different position.
Register a Player Delegate
player.addDelegate(self)
Implement AdPlayerDelegate (which combines PlayerDelegate + AdEventHandlerDelegate) to receive both content and ad events:
extension MyViewController: AdPlayerDelegate {
// MARK: - PlayerDelegate
func playerStateDidChange(state: PlayerState) {
// .idle, .loaded, .playing, .paused, .buffering, .ended, .error
}
func playerBufferStateDidChange(isBuffering: Bool) { }
func playerDurationDidChange(duration: TimeInterval) { }
func playerDidFail(with error: FLError) {
print("Playback error: \(error.localizedDescription)")
}
func playerDidFinishPlaying() { }
// MARK: - AdEventHandlerDelegate
func adPlayerDidStart(adBreak: AdBreak) {
print("Ad break started — \(adBreak.totalAds) ad(s), position: \(adBreak.position)")
}
func adPlayerDidStart(ad: Ad) {
print("Ad started — id: \(ad.adId ?? ""), duration: \(ad.duration)s")
}
func adPlayer(ad: Ad?, didChange progress: TimeInterval) {
// Update your ad progress UI
}
func adPlayer(ad: Ad?, didChangeBuffer isBuffering: Bool) { }
func adPlayer(ad: Ad?, didTrack event: AdTrackingEvent) {
// .adFirstQuartile, .adMidpoint, .adThirdQuartile, etc.
}
func adPlayerDidEnd(ad: Ad) { }
func adPlayerDidEnd(adBreak: AdBreak) {
print("Ad break ended")
}
func adPlayer(ad: Ad?, didFailWith error: Error) {
print("Ad error: \(error.localizedDescription)")
}
func adPlayerDidProvide(cuepoints: [Int]) {
// Draw cue-point markers on scrub bar
}
// MARK: - AdPlayerDelegate
func willLoadCuratedStreamAsset(_ urlAsset: AVURLAsset) {
// Called just before the stitched SSAI stream is loaded into the player
}
}
Session Shutdown
Always shut down the Broadpeak session when playback ends to release SmartLib resources:
// In your player teardown / viewWillDisappear / deinit
player.broadpeakSession.shutDown()
player.removeDelegate(self)
Ad Delegates and Event Listeners
Ad Information (Ad protocol)
Every Ad object delivered via delegate callbacks exposes:
| Property | Type | Description |
|---|---|---|
| adId | String? | Unique ad identifier from VAST. |
| duration | TimeInterval | Ad duration in seconds. |
| title | String? | Ad title from VAST metadata. |
| isSkippable | Bool | Whether the ad can be skipped. |
| adBreak | AdBreak? | The parent ad break. |
| vastProperties | VASTProperties? | VAST metadata (position, autoplay, etc.). |
Ad Break Information (AdBreak protocol)
| Property | Type | Description |
|---|---|---|
| position | AdPositio` | .preRoll, .midRoll, or .postRoll. |
| startTime | TimeInterval | Start time in seconds. |
| duration | TimeInterval | Total ad break duration in seconds. |
| totalAds | Int | Total number of ads in this break. |
Ad Cue Points
// Retrieve current cue points (preroll=0, postroll=-1, midroll=seconds)
let cuePoints = player.getCuePoints()
Skip / Discard
player.skipAd() // Skips the current ad if marked as skippable; otherwise no-op
player.discardAdBreak() // Immediately discards the entire current ad break and resumes content
Open Measurement (OM SDK) Integration
The Broadpeak player integrates with the IAB Open Measurement SDK automatically via the SmartLib-OMSDK framework (iOS only). Your app only needs to:
- Link
SmartLib-OMSDKin your target's frameworks (see Installation). - Assign an
omEventDelegatethat implementsOpenMeasurementEventDelegate. - Register friendly obstructions for any UI views that overlay the ad video.
Assign the OM Event Delegate
player.omEventDelegate = myOMSDKCoordinator
OpenMeasurementEventDelegate receives:
public protocol OpenMeasurementEventDelegate {
/// Called when a new ad starts — begin an OM session here.
func adVerificationDidStart(
adVerification: [AdVerification]?,
vastProperties: VASTProperties,
adVideoProperties: AdVideoProperties
)
/// Called when an ad ends — end the OM session here.
func adVerificationDidEnd()
/// Called for every IAB tracking event.
func adDidTrackEvent(event: AdTrackingEvent)
}
The player fires all standard OMID tracking events automatically:
| Event | When fired |
|---|---|
| adBreakStart | onAdBreakBegin from SmartLib |
| adStart | onAdBegin — after adVerificationDidStart |
| adFirstQuartile / adMidpoint / adThirdQuartile | Progress crosses 25% / 50% / 75% of ad duration |
| adEnd | onAdEnd — after adVerificationDidEnd |
| adBreakEnd | onAdBreakEnd |
| adPause / adResume | Player state transitions while ad is active |
| adMute / adUnmute | Volume crosses 0 boundary while ad is active |
Register Friendly Obstructions
Register any UI elements that legitimately overlap the ad video (controls, close buttons, etc.) so OMID does not penalize viewability:
let controlsScope = AdOverlayUIScope(
view: playerControlsView,
purpose: .playbackControls,
reason: "Player controls overlay"
)
let closeScope = AdOverlayUIScope(
view: closeButton,
purpose: .closeAd,
reason: "Ad close button"
)
player.registerFriendlyObstraction(adOverlayScope: [controlsScope, closeScope])
Unregister when playback ends:
player.unregisterAllFriendlyObstractions()
registerFriendlyObstraction is safe to call before or after the session starts. If called before the Broadpeak session is active, views are buffered and registered automatically once the session is ready.
AdOverlayPurpose values:
| Case | Description |
|---|---|
| .playbackControls | Player transport controls |
| .closeAd | Ad close/dismiss button |
| .notVisible | View is intentionally off-screen |
| .other | Any other purpose (provide a descriptive reason string) |
Google PAL Integration
Broadpeak PAL SDK Integration enables Google PAL (Programmatic Access Library) nonce generation to improve ad targeting.
Create a PAL Configuration
import FLPlayerInterface
let palConfiguration = PALConfiguration(
willAdAutoPlay: .true,
willAdPlayMuted: .false,
continuousPlayback: .false,
descriptionUrl: URL(string: "https://example.com/content-page")!,
iconsSupported: true,
nonceLengthLimit: 0, // 0 = no limit
omidPartnerName: "YourCompany",
omidPartnerVersion: "1.0.0",
omidVersion: "1.4.9",
playerType: "FLPlayer",
playerVersion: "8.0.0",
ppid: "user-123",
videoHeight: 1080,
videoWidth: 1920,
sessionID: nil,
allowStorage: consentGranted // true only after user consent obtained
)
| Property | Type | Required | Description |
|---|---|---|---|
| descriptionUrl | URL | Yes | URL of the page/content where the ad will play. |
| playerVersion | String | Yes | Player version string forwarded to Google. |
| ppid | String | Yes | Publisher-provided ID for audience targeting. |
| willAdAutoPlay | PALFlag? | No | Whether the ad plays without user interaction (.true, .false, or .auto). |
| willAdPlayMuted | PALFlag? | No | Whether the ad plays muted (.true, .false, or .auto). |
| continuousPlaybac` | PALFlag? | No | Playlist/TV-like continuous playback mode (.true, .false, or .auto). |
| iconsSupported | Bool? | No | Whether VAST icons are rendered by the player. |
| nonceLengthLimit | UInt? | No | Maximum nonce length. 0 = no limit. |
| playerType | String? | No | Player name string forwarded to Google. |
| videoHeight / videoWidth | UInt? | No | Video element dimensions in pixels. |
| omidPartnerName | String? | No | OMID partner name for measurement. |
| omidPartnerVersion | String? | No | OMID partner version. |
| omidVersion | String? | No | OMID SDK version integrated in the player. |
| sessionID | String? | No | UUID string for frequency capping within a user session. |
| allowStorage | Bool? | No | Google PAL TCFv2 consent. Overrides the global initializationData.allowStorage for this session. Defaults to false. |
PALFlag enum values:
| Case | Description |
|---|---|
| .auto | Unknown / not provided — lets the SDK determine optimal behaviour. |
| .true | Explicitly enables the feature. |
| .false | Explicitly disables the feature. |
allowStorage controls whether the Google PAL SDK may use local storage for user identification. Set true only after obtaining user consent in accordance with applicable regulations (GDPR/CCPA).
Ad Playback Policy
AdPlaybackPolicy governs what happens to ads when the user seeks, fast-forwards, rewinds, or pauses.
let adPlaybackPolicy = AdPlaybackPolicy(
fastForwardRule: .TRICK_FW_PLAY_LAST, // play the last ad in a skipped break
rewindRule: .TRICK_RW_SKIP_ALL, // skip all ads when rewinding
interruptRule: .INTERRUPT_RESUME, // resume ad on return from background
autoSeekRule: .AUTO_SEEK_PLAY_PREROLL, // play pre-roll on auto-seek past it
repeatRule: .REPEAT_PLAY_ONCE // play each ad break only once
)
let player = FLAdvertisingBroadpeakFactor.createBroadpeakPlayer(
initialisationUrl: "https://stream.cdn.example.com/live/index.m3u8",
fallbackURL: URL(string: "https://origin.cdn.example.com/live/index.m3u8")!,
sessionData: sessionData,
adPlaybackPolicy: adPlaybackPolicy
)
If adPlaybackPolicy is nil, the default policy is used (all rules use their default values).
Picture-in-Picture (PiP)
To maintain ad playback continuity during PiP transitions, pass pipSession: true when creating session data for the secondary PiP player:
let pipSessionData = FLAdvertisingBroadpeakFactor.broadpeakSessionData(
pipSession: true,
customParams: sessionData.customParams,
adParams: sessionData.adParams,
palConfiguration: palConfiguration
)
Listen to PiP lifecycle events via PlayerDelegate:
func playerDidStartPictureInPicture() { /* swap to PiP player */ }
func playerDidStopPictureInPicture() { /* restore main player */ }
func restoreUserInterfaceForPictureInPictureStopWithCompletionHandler(
completionHandler: @escaping (Bool) -> Void
) {
// restore your UI, then call completionHandler(true)
completionHandler(true)
}
GDPR & Privacy
Control Broadpeak analytics data reporting via gdprPreference in initialization data:
| Value | Behaviour |
|---|---|
| .GDPR_CLEAR | No encryption (default) |
| .GDPR_ENCRYPTED | Encrypted — decryptable on request |
| .GDPR_ANONYMIZED | Anonymized — no decryption possible |
| .GDPR_DELETE | Analytics report is empty |
Control local storage consent per session via allowStorage in the session data. Defaults to false. Only set true after obtaining user consent in accordance with applicable regulations (GDPR/CCPA).
Error Handling
All errors from createBroadpeakPlayer are delivered via the adPlayer(ad:didFailWith:) delegate callback.
| Error Code | Value | Description |
|---|---|---|
| SMARTLIB_INSTANCE_NOT_INITIALIZED | 0x0A01 | createInstance was not called before player creation. |
| GET_URL_FAILURE | 0x0A02 | SmartLib failed to resolve the Broadpeak stream URL. |
| INVALID_PLAYBACK_URL | 0x0A03 | The asset's contentUrl is not a valid URL. |
extension MyViewController: AdPlayerDelegate {
func adPlayer(ad: Ad?, didFailWith error: Error) {
if let bpError = error as? BroadpeakErrorCodes {
switch bpError {
case .SMARTLIB_INSTANCE_NOT_INITIALIZED:
// Ensure broadpeakSessionManager.createInstance(...) was called at launch
break
case .GET_URL_FAILURE:
// SmartLib could not resolve stream URL — check CDN domain configuration
break
case .INVALID_PLAYBACK_URL:
// Verify the asset contentUrl is a valid URL string
break
}
}
}
}
Complete Integration Example
import FLAdvertisingBroadpeak
import FLPlayerInterface
// ── AppDelegate ───────────────────────────────────────────────────────────────
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let initData = FLAdvertisingBroadpeakFactor.initializationData(
broadpeakDomainNames: .Custom(["stream.cdn.broadpeak.io"]),
gdprPreference: .GDPR_CLEAR,
loggerLevel: .levelNone
)
FLAdvertisingBroadpeakFactor.broadpeakSessionManager.createInstance(initializationData: initData)
return true
}
// ── PlayerViewController ──────────────────────────────────────────────────────
class PlayerViewController: UIViewController {
var broadpeakPlayer: BroadpeakPlayer?
func startPlayback(initialisationUrl: String, fallbackURL: URL) {
// 1. PAL configuration (optional)
let palConfig = PALConfiguration(
willAdAutoPlay: .true,
willAdPlayMuted: .false,
continuousPlayback: .false,
descriptionUrl: fallbackURL,
iconsSupported: true,
nonceLengthLimit: 0,
omidPartnerName: "YourCompany",
omidPartnerVersion: "1.0.0",
omidVersion: "1.4.9",
playerType: "FLPlayer",
playerVersion: "8.0.0",
ppid: "user-123",
videoHeight: 1080,
videoWidth: 1920,
sessionID: nil,
allowStorage: false
)
// 2. Session data
let sessionData = FLAdvertisingBroadpeakFactor.broadpeakSessionData(
pipSession: false,
customParams: ["env": "production"],
adParams: ["targeting": "sports"],
palConfiguration: palConfig
)
// 3. Ad playback policy
let policy = AdPlaybackPolicy(
fastForwardRule: .TRICK_FW_PLAY_LAST,
rewindRule: .TRICK_RW_SKIP_ALL,
interruptRule: .INTERRUPT_RESUME
)
// 4. Create player — errors are reported via adPlayer(ad:didFailWith:)
broadpeakPlayer = FLAdvertisingBroadpeakFactor.createBroadpeakPlayer(
initialisationUrl: initialisationUrl,
fallbackURL: fallbackURL,
sessionData: sessionData,
adPlaybackPolicy: policy
)
// 5. Add delegate BEFORE play() so no events or errors are missed
broadpeakPlayer?.addDelegate(self)
// 6. OM SDK delegate
broadpeakPlayer?.omEventDelegate = myOMCoordinator
// 7. Register friendly obstructions
let controlsOverlay = AdOverlayUIScope(
view: playerControlsView,
purpose: .playbackControls,
reason: "Transport controls"
)
broadpeakPlayer?.registerFriendlyObstraction(adOverlayScope: [controlsOverlay])
attachPlayer()
}
func attachPlayer() {
guard let player = broadpeakPlayer else { return }
playerContainerView.addSubview(player.playbackView)
player.playbackView.frame = playerContainerView.bounds
player.play()
}
func stopPlayback() {
broadpeakPlayer?.stop()
broadpeakPlayer?.unregisterAllFriendlyObstractions()
broadpeakPlayer?.broadpeakSession.shutDown()
broadpeakPlayer?.removeDelegate(self)
broadpeakPlayer?.playbackView.removeFromSuperview()
broadpeakPlayer = nil
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopPlayback()
}
}
// ── AdPlayerDelegate ──────────────────────────────────────────────────────────
extension PlayerViewController: AdPlayerDelegate {
func playerStateDidChange(state: PlayerState) { /* update UI */ }
func playerDidFail(with error: FLError) { print("Error: \(error)") }
func playerDidFinishPlaying() { stopPlayback() }
func adPlayer(ad: Ad?, didFailWith error: Error) {
print("Ad/init error: \(error.localizedDescription)")
// Handle BroadpeakErrorCodes here if needed
}
func adPlayerDidStart(adBreak: AdBreak) {
print("▶ Ad break: \(adBreak.totalAds) ad(s) at \(adBreak.position)")
// hide scrub bar, show "Ad" label
}
func adPlayerDidStart(ad: Ad) {
print("▶ Ad: \(ad.adId ?? "") — \(ad.duration)s")
}
func adPlayer(ad: Ad?, didChange progress: TimeInterval) {
// update ad countdown UI
}
func adPlayerDidEnd(adBreak: AdBreak) {
// restore scrub bar
}
func adPlayerDidEnd(ad: Ad) { }
func adPlayer(ad: Ad?, didTrack event: AdTrackingEvent) { }
func adPlayer(ad: Ad?, didChangeBuffer isBuffering: Bool) { }
func adPlayerDidProvide(cuepoints: [Int]) {
// draw cue-point markers on scrub bar
}
func willLoadCuratedStreamAsset(_ urlAsset: AVURLAsset) { }
func playerBufferStateDidChange(isBuffering: Bool) { }
func playerSeekStateDidChange(isSeeking: Bool) { }
func playerRateDidChange(rate: Float) { }
func playerBufferedRangeDidChange(range: Range<TimeInterval>) { }
func playerDurationDidChange(duration: TimeInterval) { }
func playerAudioVolumeDidChange(volume: Float) { }
func playerAudioRouteDidChange(from: AVAudioSession.Port?, to: AVAudioSession.Port?) { }
func playerDidReceiveTimedMetadata(groups: [AVTimedMetadataGroup]) { }
func playerDidReceiveDateRangeMetadata(groups: [AVDateRangeMetadataGroup]) { }
func playerTrackDidChange(mediaTrack: MediaTrack?) { }
func onTrackAvailabilityChanged(forTrack type: MediaTrackType) { }
}