Skip to content

Usage (iOS)

You create a SurfmeterAnalyticsBuilder for your AVPlayer instance, build a SurfmeterAnalytics object from it, and keep that object for as long as the video plays. The SDK observes the player, sends data at a regular interval, and sends a final report when the session ends. When playback is over, call finish().

Demo App

A demo app is provided with the SDK to illustrate its usage. You can find it as AVFoundationExample.zip inside the AVPlayerAnalytics-DemoApp-$version.zip archive. It is a SwiftUI app that plays a list of HLS streams and shows how to attach the analytics, how to switch between videos, and how to finish a session. The interesting file is SurfmeterAnalyticsContentView.swift, which you can take into your own app as a starting point.

To build the demo app:

  1. Unzip AVFoundationExample.zip and open AVFoundationExample.xcodeproj in Xcode.
  2. The project refers to the SDK as a local Swift package at ../AVPlayerAnalytics, which is not part of the archive. Select that entry under "Package Dependencies" in the project navigator, remove it, and add the released package instead, as described in the installation instructions.
  3. Copy Local.xcconfig.example to Local.xcconfig next to it and put your API key in there. You can also set your own endpoint; if you leave it empty, the SDK uses its default one. BuildConfig.xcconfig pulls this file in if it exists, and both values are copied into Info.plist at build time and read back in Configuration.swift, so that neither ends up in the source code.
  4. Set your own bundle identifier and signing team on the target, then run the app on a simulator or a device.

URLs in xcconfig files

In an xcconfig file, // starts a comment, so a URL has to escape it as /$()/:

API_ENDPOINT = https:/$()/surfmeter-server.example.com/client_analytics_api/v1
API_KEY = your-api-key

Basic Example

In the simplest case, you create a builder, attach it to your AVPlayer instance, and build:

import AVFoundation
import AVPlayerAnalytics

let player = AVPlayer(url: URL(string: "https://example.com/master.m3u8")!)

let analytics = SurfmeterAnalyticsBuilder(
    player: player,
    apiKey: "your-api-key"
)
.setService(service: "your-service")
.build()

player.play()

Keep a strong reference

Store the SurfmeterAnalytics object in a property of your view or view model, not in a local variable. When the object is deallocated, it stops observing the player and no more data is sent. The same holds for the listener described below, which the SDK only holds weakly.

Create the analytics object on the main thread, and build it before you start playback, so that the SDK sees the whole session.

Builder Options

Besides the API key and the player, the builder takes the following options. All of them are optional, and all of them return the builder, so you can chain them:

  • setService(service:) sets the name of the measured service, which is how the data is grouped in the dashboard. If you leave it out, default is used.
  • setEndpoint(endpoint:) sends the data to your own server instead of the default one. This is the only builder method that can throw, see below.
  • setMetadata(metadata:) attaches custom metadata to the session.
  • setVideoTitle(videoTitle:) sets the title of the first video.
  • setChannelAttributes(channelId:channelTitle:) sets the channel of the first video.
  • setSendingInterval(sendingInterval:) changes how often data is sent, in seconds. The default is 20 seconds.
  • setFingerprint(fingerprint:) overrides the device identifier. By default the SDK uses the vendor identifier of the device, which is stable for your apps on that device.
  • setAnalyticsListener(listener:) attaches a listener for state changes, errors and log messages.

With a Custom Endpoint

If you run your own Surfmeter server, point the SDK at it. The URL must be a valid URL and must not end with a slash, otherwise setEndpoint throws, so the call needs a try and a do/catch block:

do {
    analytics = try SurfmeterAnalyticsBuilder(
        player: player,
        apiKey: "your-api-key"
    )
    .setService(service: "your-service")
    .setEndpoint(endpoint: "https://surfmeter-server.example.com/client_analytics_api/v1")
    .build()
} catch {
    print("Could not set up analytics: \(error)")
}

With Custom Metadata

You can attach custom metadata to your analytics data in the builder:

let analytics = SurfmeterAnalyticsBuilder(
    player: player,
    apiKey: "your-api-key"
)
.setService(service: "your-service")
.setMetadata(metadata: ["userId": "1234567890"])
.build()

These can be retrieved in the dashboard, and are very useful for segmenting your data for analysis purposes.

Note

Please make sure that if you send personal data such as customer IDs, names, or email addresses, you have the necessary consent of the user to do so.

With Video and Channel Attributes

If you know what is being played, pass the title and the channel, so that the data can be broken down by content in the dashboard:

let analytics = SurfmeterAnalyticsBuilder(
    player: player,
    apiKey: "your-api-key"
)
.setService(service: "your-service")
.setVideoTitle(videoTitle: "Big Buck Bunny")
.setChannelAttributes(channelId: "42", channelTitle: "Demo Channel")
.build()

Analytics Listener

You can implement the SurfmeterAnalyticsListener protocol to receive callbacks about the analytics state:

import AVPlayerAnalytics
import OSLog

class SurfmeterAnalyticsListenerImpl: SurfmeterAnalyticsListener {
    func onSendingData() {
        print("Sending data")
    }

    func onError(_ error: String) {
        print("Error: \(error)")
    }

    func onStateChanged(_ state: AnalyticsState) {
        print("State changed: \(state)")
    }

    func onLog(type: OSLogType, tag: String, log: String) {
        // if you want more logs:
        // print("Log: \(type) \(tag) \(log)")
    }
}

Attach it to the builder:

let listener = SurfmeterAnalyticsListenerImpl()

let analytics = SurfmeterAnalyticsBuilder(
    player: player,
    apiKey: "your-api-key"
)
.setService(service: "your-service")
.setAnalyticsListener(listener: listener)
.build()

The state is STATE_READY before playback starts, STATE_ANALYZING while a video is being measured, and STATE_ENDED once the session is over. The callbacks do not always arrive on the main thread, so dispatch to the main queue before you touch the user interface from them.

Managing Multiple Videos

When the player moves to another video, the SDK closes the running session, sends its data, and starts a new one. Set the attributes of the new video immediately after the transition, so that they are attached to the right session:

player.replaceCurrentItem(with: AVPlayerItem(url: nextVideoUrl))

analytics?.setVideoTitle(videoTitle: "Second video")
analytics?.setChannelAttributes(channelId: "43", channelTitle: "Another Channel")
analytics?.setMetadata(metadata: ["userId": "1234567890"])

player.play()

Alternatively, you can end the session yourself and build a new analytics object for the next video. In that case use finish(completion:), which tells you when the previous session has been wrapped up:

analytics?.finish { [weak self] in
    guard let self else { return }

    self.player.replaceCurrentItem(with: AVPlayerItem(url: nextVideoUrl))

    self.analytics = SurfmeterAnalyticsBuilder(
        player: self.player,
        apiKey: "your-api-key"
    )
    .setService(service: "your-service")
    .setVideoTitle(videoTitle: "Second video")
    .build()

    self.player.play()
}

Ending a Session

When playback is over, or when the view that owns the player goes away, call finish():

analytics?.finish()
analytics = nil

An instance must not be used again after finish(). Release it along with the player, and build a new one for the next session.

Error Handling

If your app runs into an error that makes the session unusable, end it with abort(reason:) instead of finish(), and pass a reason that shows up with the measurement:

analytics?.abort(reason: "Player error occurred")
analytics = nil

For errors that do not end the session, report them instead. They are collected and sent along with the next batch of data:

analytics?.onPlaybackError(
    message: "CDN timeout after 30s",  // human-readable description
    errorType: "cdn_error",            // error category
    errorCode: "504"                   // error code as a string
)

Both errorType and errorCode are optional. Multiple errors between two sends all accumulate. This is useful for problems that AVPlayer does not raise itself, such as errors in your own DRM handling or in your application logic.

API Docs

The SDK ships with an API reference for every public type, generated with DocC. See the installation instructions for how to read it.