Skip to main content

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

RequirementDetails
iOS14.0+
tvOS14.0+
Swift5.8+
Xcode26.0+
Broadpeak SmartLib6.0.2 (exact)
FLPlayerLatest 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:

ValueBehaviour
.AllAll session URLs are treated as Broadpeak CDN streams
.NoneNo URLs are intercepted (effectively disables SSAI)
.Custom(["cdn.example.com"])Only URLs matching these hostnames are intercepted

analyticsAddress:

ValueBehaviour
.NoneAnalytics disabled
.Custom(["https://analytics.host"])Send Broadpeak analytics to these servers
note

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)
caution

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
)
PropertyTypeRequiredDescription
pipSessionBool?NoSet true when spawning a second session for Picture-in-Picture.
customParams[String: String]?NoCustom key-value pairs forwarded to Broadpeak analytics.
adParams[String: String]?NoCustom parameters appended to ad request URLs.
palConfigurationPALSessionConfigurationNoGoogle 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
ParameterTypeRequiredDescription
initialisationUrlStringYesThe Broadpeak SSAI stream URL. SmartLib resolves this asynchronously to the ad-stitched playback URL.
fallbackURLURLYesDirect origin URL used as the player asset while SmartLib resolves the SSAI URL, and as a fallback on resolution failure.
sessionDataBroadpeakSessionDataYesPer-session Broadpeak configuration.
adPlaybackPolicyAdPlaybackPolicy?NoAd behaviour rules for seek, interrupt, etc.
thumbnailConfigurationContentThumbnailPreviewConfiguration?NoThumbnail preview configuration.
note

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()
note

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:

PropertyTypeDescription
adIdString?Unique ad identifier from VAST.
durationTimeIntervalAd duration in seconds.
titleString?Ad title from VAST metadata.
isSkippableBoolWhether the ad can be skipped.
adBreakAdBreak?The parent ad break.
vastPropertiesVASTProperties?VAST metadata (position, autoplay, etc.).

Ad Break Information (AdBreak protocol)

PropertyTypeDescription
positionAdPositio`.preRoll, .midRoll, or .postRoll.
startTimeTimeIntervalStart time in seconds.
durationTimeIntervalTotal ad break duration in seconds.
totalAdsIntTotal 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:

  1. Link SmartLib-OMSDK in your target's frameworks (see Installation).
  2. Assign an omEventDelegate that implements OpenMeasurementEventDelegate.
  3. 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:

EventWhen fired
adBreakStartonAdBreakBegin from SmartLib
adStartonAdBegin — after adVerificationDidStart
adFirstQuartile / adMidpoint / adThirdQuartileProgress crosses 25% / 50% / 75% of ad duration
adEndonAdEnd — after adVerificationDidEnd
adBreakEndonAdBreakEnd
adPause / adResumePlayer state transitions while ad is active
adMute / adUnmuteVolume 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()
note

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:

CaseDescription
.playbackControlsPlayer transport controls
.closeAdAd close/dismiss button
.notVisibleView is intentionally off-screen
.otherAny 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
)
PropertyTypeRequiredDescription
descriptionUrlURLYesURL of the page/content where the ad will play.
playerVersionStringYesPlayer version string forwarded to Google.
ppidStringYesPublisher-provided ID for audience targeting.
willAdAutoPlayPALFlag?NoWhether the ad plays without user interaction (.true, .false, or .auto).
willAdPlayMutedPALFlag?NoWhether the ad plays muted (.true, .false, or .auto).
continuousPlaybac`PALFlag?NoPlaylist/TV-like continuous playback mode (.true, .false, or .auto).
iconsSupportedBool?NoWhether VAST icons are rendered by the player.
nonceLengthLimitUInt?NoMaximum nonce length. 0 = no limit.
playerTypeString?NoPlayer name string forwarded to Google.
videoHeight / videoWidthUInt?NoVideo element dimensions in pixels.
omidPartnerNameString?NoOMID partner name for measurement.
omidPartnerVersionString?NoOMID partner version.
omidVersionString?NoOMID SDK version integrated in the player.
sessionIDString?NoUUID string for frequency capping within a user session.
allowStorageBool?NoGoogle PAL TCFv2 consent. Overrides the global initializationData.allowStorage for this session. Defaults to false.

PALFlag enum values:

CaseDescription
.autoUnknown / not provided — lets the SDK determine optimal behaviour.
.trueExplicitly enables the feature.
.falseExplicitly disables the feature.
note

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:

ValueBehaviour
.GDPR_CLEARNo encryption (default)
.GDPR_ENCRYPTEDEncrypted — decryptable on request
.GDPR_ANONYMIZEDAnonymized — no decryption possible
.GDPR_DELETEAnalytics 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 CodeValueDescription
SMARTLIB_INSTANCE_NOT_INITIALIZED0x0A01createInstance was not called before player creation.
GET_URL_FAILURE0x0A02SmartLib failed to resolve the Broadpeak stream URL.
INVALID_PLAYBACK_URL0x0A03The 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) { }
}