A library for sending ad beacons from the client-side. Works with both traditional SSAI and HLS interstitials. Compatible with iOS/tvOS 15 and above.
- HarmonicClientSideAdTracking
Using Swift Package Manager, enter the URL of this package:
https://github.com/harmonicinc-com/client-side-ad-tracking-ios-lib
-
Import the library
import HarmonicClientSideAdTracking -
Create an
AdBeaconingSessionobject:let mySession = AdBeaconingSession()
-
Optionally, confgure the following options in your session:
mySession.keepPodsForMs = 1_000 * 60 * 60 * 2 // Double: duration to cache ad metadata, defaults to 2 hours mySession.metadataUpdateInterval = 4 // TimeInterval: how often to fetch the ad metadata, defaults to 4 seconds
-
Optionally, set the session's player to your own instance of AVPlayer:
let myAVPlayer = AVPlayer() mySession.player = myAVPlayer
-
Set the session's media URL to the master playlist of an HLS stream:
mySession.mediaUrl = "<hls-master-playlist-url>"
- Note that the
AdBeaconingSessionobject will then do the following:- Try to obtain a manifest URL with a session ID (if the provided
mediaUrldoesn't already contain one); - Try to obtain the corresponding metadata URL with the session ID.
- Try to obtain a manifest URL with a session ID (if the provided
- Note that the
-
Observe the session's
manifestUrlby using the.onReceive(_:perform:)method in SwiftUI (for UIKit, please see the example below). When it is set and not empty, create anAVPlayerItemwith the URL and set it in the player:if !manifestUrl.isEmpty { let myPlayerItem = AVPlayerItem(url: URL(string: manifestUrl)!) mySession.player.replaceCurrentItem(with: myPlayerItem) }
-
Create a
HarmonicAdTrackerobject and initialize it with the session created above:let adTracker: HarmonicAdTracker? adTracker = HarmonicAdTracker(session: mySession)
-
Start the ad tracker:
adTracker?.start()
-
Start playing and beacons will be sent when ads are played:
mySession.player.play()
-
You may observe the following information from the session instance:
- To get the URLs with the session ID:
URLs available:
let sessionInfo = mySession.sessionInfo
sessionInfo.mediaUrl // String sessionInfo.manifestUrl // String sessionInfo.adTrackingMetadataUrl // String
- To get the list of
AdBreaks returned from the ad metadata along with the status of the beaconing for each event.For example, in the firstlet adPods = mySession.adPods
AdBreakofadPods:In the firstadPods[0].id // String adPods[0].startTime // Double: millisecondsSince1970 adPods[0].duration // Double: milliseconds adPods[0].ads // [Ad]
AdofadPods[0].ads:In the firstads[0].id // String ads[0].startTime // Double: millisecondsSince1970 ads[0].duration // Double: milliseconds ads[0].trackingEvents // [TrackingEvent]
TrackingEventofads[0].trackingEvents:trackingEvents[0].event // EventType trackingEvents[0].startTime // Double: millisecondsSince1970 trackingEvents[0].duration // Double: milliseconds trackingEvents[0].signalingUrls // [String] trackingEvents[0].reportingState // ReportingState
- To get the latest DataRange returned from the ad metadata.
To get the time in
let latestDataRange = mySession.latestDataRange
millisecondsSince1970:latestDataRange.start // Double: millisecondsSince1970 latestDataRange.end // Double: millisecondsSince1970
- To get the status and information of the player.
Information available:
let playerObserver = mySession.playerObserver
playerObserver.currentDate // Date playerObserver.playhead // Double: millisecondsSince1970 playerObserver.primaryStatus // AVPlayer.TimeControlStatus playerObserver.hasInterstitialEvents // Bool playerObserver.interstitialStatus // AVPlayer.TimeControlStatus playerObserver.interstitialDate // Double: millisecondsSince1970 playerObserver.interstitialStoppedDate // Double: millisecondsSince1970 playerObserver.interstitialStartTime // Double: seconds playerObserver.interstitialStopTime // Double: seconds playerObserver.currentInterstitialDuration // Double: milliseconds
- To get the messages logged by the library.
For example, in the first
let logMessages = mySession.logMessages
LogMessage:logMessages[0].timeStamp // Double: secondsSince1970 logMessages[0].message // String logMessages[0].isError // Bool logMessages[0].error // HarmonicAdTrackerError?: the error object if available
- To observe errors encountered by the library:
// SwiftUI .onReceive(mySession.$latestError) { error in if let error = error { switch error { case .networkError(let message): print("Network error: \(message)") case .metadataError(let message): print("Metadata error: \(message)") case .beaconError(let message): print("Beacon error: \(message)") } } } // UIKit / Combine var cancellables = Set<AnyCancellable>() mySession.$latestError .sink { error in if let error = error { // Handle error print("Ad tracking error: \(error)") } } .store(in: &cancellables)
- To get the URLs with the session ID:
-
Stop the ad tracker when it is not needed:
adTracker?.stop()
-
(Optional) Manually cleanup the session before setting it to nil to ensure immediate memory cleanup:
mySession.cleanup() mySession = nil // or set to a new session
- Note: This step is optional as the session will automatically cleanup when deallocated, but calling
cleanup()explicitly ensures immediate cleanup of player observers and prevents potential memory leaks.
- Note: This step is optional as the session will automatically cleanup when deallocated, but calling
The library provides multiple ways to monitor and handle errors:
The latestError property on AdBeaconingSession publishes the most recent error encountered:
SwiftUI Example:
import SwiftUI
import HarmonicClientSideAdTracking
struct ContentView: View {
@StateObject private var mySession = AdBeaconingSession()
var body: some View {
VideoPlayer(player: mySession.player)
.onReceive(mySession.$latestError) { error in
if let error = error {
handleError(error)
}
}
}
private func handleError(_ error: HarmonicAdTrackerError) {
switch error {
case .networkError(let message):
print("Network error: \(message)")
// Handle network-related errors (e.g., failed to load media or init session)
case .metadataError(let message):
print("Metadata error: \(message)")
// Handle metadata-related errors (e.g., invalid ad metadata)
case .beaconError(let message):
print("Beacon error: \(message)")
// Handle beacon sending errors (e.g., failed to send tracking beacon)
}
}
}UIKit Example:
import UIKit
import Combine
import HarmonicClientSideAdTracking
class ViewController: UIViewController {
private var mySession = AdBeaconingSession()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// Observe errors
mySession.$latestError
.compactMap { $0 } // Filter out nil values
.sink { [weak self] error in
self?.handleError(error)
}
.store(in: &cancellables)
}
private func handleError(_ error: HarmonicAdTrackerError) {
switch error {
case .networkError(let message):
// Handle network error
showAlert(title: "Network Error", message: message)
case .metadataError(let message):
// Handle metadata error
showAlert(title: "Metadata Error", message: message)
case .beaconError(let message):
// Handle beacon error
print("Beacon error (non-critical): \(message)")
}
}
private func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}For detailed logging information, you can observe the logMessages array which includes all errors with their associated error objects:
mySession.$logMessages
.sink { messages in
for message in messages where message.isError {
print("Error at \(message.timeStamp): \(message.message)")
if let error = message.error {
// Access the structured error object
print("Error type: \(error)")
}
}
}
.store(in: &cancellables)The library defines three types of errors:
networkError(String): Network-related errors such as failed HTTP requests or connection issues, or when session initialization failsmetadataError(String): Errors related to fetching or parsing ad metadatabeaconError(String): Errors related to sending tracking beacons
In addition to automatic beacon firing for progress events (start, firstQuartile, midpoint, thirdQuartile, complete), the library supports user-triggered beacon events. These events should be reported by your application when the user interacts with the player during ad playback.
The following user-triggered beacon methods are available on HarmonicAdTracker:
| Method | Description |
|---|---|
reportMute() |
Call when the user mutes the player |
reportUnmute() |
Call when the user unmutes the player |
reportPause() |
Call when the user pauses the player |
reportResume() |
Call when the user resumes the player |
These methods are async and return a Bool indicating whether the beacon was sent:
- Returns
trueif the beacon was successfully sent - Returns
falseif no ad is currently playing or no matching tracking event exists in the ad metadata
Note: These beacon methods will only send beacons if:
- An ad is currently playing (the playhead is within an ad's time range)
- The ad metadata includes tracking events for the respective event type (mute, unmute, pause, resume)
In these examples, ad beacons will be sent while the stream is being played, but no UI is shown to indicate the progress of beaconing.
import SwiftUI
import AVKit
import HarmonicClientSideAdTracking
struct ContentView: View {
@StateObject private var mySession = AdBeaconingSession()
@State private var adTracker: HarmonicAdTracker?
var body: some View {
VideoPlayer(player: mySession.player)
.onAppear {
mySession.mediaUrl = "<hls-master-playlist-url>"
adTracker = HarmonicAdTracker(session: mySession)
}
.onReceive(mySession.sessionInfo.$manifestUrl) { manifestUrl in
if !manifestUrl.isEmpty {
let myPlayerItem = AVPlayerItem(url: URL(string: manifestUrl)!)
mySession.player.replaceCurrentItem(with: myPlayerItem)
mySession.player.play()
adTracker?.start()
}
}
.onDisappear {
adTracker?.stop()
}
}
}import UIKit
import AVKit
import Combine
import HarmonicClientSideAdTracking
class ViewController: UIViewController {
private var mySession = AdBeaconingSession()
private var adTracker: HarmonicAdTracker?
private var sessionSub: AnyCancellable?
override func viewDidAppear(_ animated: Bool) {
mySession.mediaUrl = "<hls-master-playlist-url>"
adTracker = HarmonicAdTracker(session: mySession)
let controller = AVPlayerViewController()
controller.player = mySession.player
present(controller, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
sessionSub = mySession.$sessionInfo.sink { [weak self] sessionInfo in
guard let self = self else { return }
if !sessionInfo.manifestUrl.isEmpty {
let myPlayerItem = AVPlayerItem(url: URL(string: sessionInfo.manifestUrl)!)
mySession.player.replaceCurrentItem(with: myPlayerItem)
mySession.player.play()
adTracker?.start()
}
}
}
override func viewWillAppear(_ animated: Bool) {
adTracker?.stop()
}
}This example demonstrates using your own AVPlayer instance instead of the session's default player, observing ad pods from the metadata API to print their IDs, and adding a custom overlay on top of the video player.
import SwiftUI
import AVKit
import HarmonicClientSideAdTracking
struct CustomPlayerView: View {
@StateObject private var mySession = AdBeaconingSession()
@State private var adTracker: HarmonicAdTracker?
@State private var customPlayer = AVPlayer()
var body: some View {
VideoPlayer(player: customPlayer, videoOverlay: {
CustomOverlay()
})
.onAppear {
mySession.player = customPlayer
mySession.mediaUrl = "<hls-master-playlist-url>"
adTracker = HarmonicAdTracker(session: mySession)
}
.onReceive(mySession.sessionInfo.$manifestUrl) { manifestUrl in
if !manifestUrl.isEmpty {
let playerItem = AVPlayerItem(url: URL(string: manifestUrl)!)
customPlayer.replaceCurrentItem(with: playerItem)
customPlayer.play()
adTracker?.start()
}
}
.onReceive(mySession.$adPods) { adPods in
for adPod in adPods {
print("Ad Pod ID: \(adPod.id)")
}
}
.onDisappear {
customPlayer.replaceCurrentItem(with: nil)
adTracker?.stop()
mySession.cleanup()
}
}
}
struct CustomOverlay: View {
var body: some View {
VStack {
Spacer()
Text("Custom Overlay")
.padding()
.background(Color.black.opacity(0.7))
.foregroundColor(.white)
}
}
}The library consists of several SwiftUI views that are used in the demo project. They are used to show how to display the progress of beacon-sending, and are not required for the ad beaconing logic to work.
This view shows a list of AdBreakViews.
-
Each
AdBreakViewindicates an ad break, and shows a list ofAdViews, each representing an ad in this ad break. -
Each
AdViewindicates an ad, and shows a list ofTrackingEventViews, each representing a tracking event in this ad. -
Each
TrackingEventViewindicates a tracking event, and shows information for this particular tracking event, including:- The event name
- The signaling URLs
- The time of the event
- The state of the beaconing of this event, which may be
idle,connecting,done, orfailed
Shows information about playback:
- Playhead
- Time to next ad beack
- If interstitials are available:
- Last interstitial's event date
- Last interstitial's start time
- Last interstitial's end time
Also, the different URLs of the session:
- Media URL (set by the user)
- Manifest URL (the redirected URL with a session ID)
- Ad tracking metadata URL
Contains a VideoPlayer with a debug overlay showing the real-world time and the latency. It also reloads by creating a new instance of player when the session's automaticallyPreservesTimeOffsetFromLive option is changed.
A demo app (that can be run on both iOS and tvOS) on how this library (including the SwiftUI views) may be used is available at the following repository: https://github.com/harmonicinc-com/client-side-ad-tracking-ios
Note
Applicable when isInitRequest in AdBeaconingSession is true (default is true).
-
The library sends a GET request to the manifest endpoint with the query param "initSession=true". For e.g., a GET request is sent to:
https://my-host/variant/v1/hls/index.m3u8?initSession=true -
The ad insertion service (PMM) responds with the URLs. For e.g.,
{ "manifestUrl": "./index.m3u8?sessid=a700d638-a4e8-49cd-b288-6809bd35a3ed&vosad_inst_id=pmm-0", "trackingUrl": "./metadata?sessid=a700d638-a4e8-49cd-b288-6809bd35a3ed&vosad_inst_id=pmm-0" } -
The library constructs the URLs by combining the host in the original URL and the relative URLs obtained. For e.g.,
Manifest URL: https://my-host/variant/v1/hls/index.m3u8?sessid=a700d638-a4e8-49cd-b288-6809bd35a3ed&vosad_inst_id=pmm-0 Metadata URL: https://my-host/variant/v1/hls/metadata?sessid=a700d638-a4e8-49cd-b288-6809bd35a3ed&vosad_inst_id=pmm-0
Note
You may obtain these URLs from the sessionInfo property of your AdBeaconingSession.