Getting started guide

Initialisation

To start using Vesper SDK you need to create an instance of VesperSDK manager first

class MyAuthManager: AuthManagerProtocol {
    func getAuthToken(completion: @escaping (String?) -> Void) {
        //implementation
    }

    func getRefreshToken(completion: @escaping (String?) -> Void) {
        //implementation
    }

    func refreshAuthToken(authToken: String, completion: @escaping (String?) -> Void) {
        //implementation
    }
}

let apiConfig = APIConfig(realm: "dce.adtech2",
                          environment: .prod,
                          apiKey: "API_KEY_HERE")

let authManager = MyAuthManager()
let config = VesperSDKConfig(apiConfig: apiConfig, authManager: authManager)
let vesperSDKManager = VesperSDKManager(config: config)

Authentication

You may have noticed that the SDK takes an AuthManagerProtocol as part of its configuration.

Ownership of the authentication flow is left to the application using the Vesper SDK. Once the app has authenticated with the user’s credential, the SDK via the AuthManager will retrieve the user’s authToken and refreshToken to facilitate interactions with the Vesper platform.

Token Refresh

When the authToken expires, the SDK will use the refreshAuthToken method on the AuthManagerProtocol to request a new token from the application. It is the application’s responsibility to request and provide the updated token back to the Vesper SDK.

In the event of an authToken refresh error, the current playback session will be stopped. The expectation will be on the app to prompt the user to sign in again.

The SDK offers a mechanism to listen for this event. We’ll take a closer look at this in the Beacons section.

Creating the Player

Once the Vesper SDK has been created, you may move on to the PlayerManager and initialize it for playback.

The PlayerManager will be our entry point for everything playback related as we will see throughout this guide. The simplest way to create it is as follows:

import VesperSDK

vesperSDKManager.createPlayerManager(userInterfaceConfig: .default(output: nil)) { result in
    switch result {
    case .success(let playerManager): break
    //maintain strong reference to playerManager for future access
    case .failure(let error): break
    //handle createPlayerManager errors
    }
}

Player configuration

Once you obtain a PlayerManager, you can access the new PlayerManagerConfig API via playerManager.config to fine-tune playback without recreating the manager.

What you can control:

  • bandwidthPolicy – override the default data saver policy (e.g. always-on data saver, higher thresholds on Wi‑Fi, etc.).
  • tracksLocalizationConfig – adjust how audio/text track labels are generated if you want to enforce custom localization logic.
  • toggles.isPipEnabled – enable or disable Picture in Picture support on the fly (see Picture in Picture).

User Interface

Default UI

The SDK ships with a default Player UI, where theming is automatically applied via settings from Vesper BackOffice.

This default UI can be configured during playerManager creation using the userInterfaceConfig parameter.

if UserInterfaceConfig.default is selected you can additionally configure UI using playerManager.uiManager if needed like so:

vesperSDKManager.createPlayerManager(userInterfaceConfig: .default(output: nil)) { [weak self] result in
guard let self else { return }
    switch result {
    case .success(let playerManager):
        self.playerManager = playerManager
        self.playerManager?.uiManager?.config.displayType = orientation != .portrait ? .max : .regular

        if let uiManager = playerManager.uiManager {
            self.setupLayout(uiManager: uiManager)
        }
    default: break
    }
}

func setupLayout(uiManager: UIManager) {
    view.addSubview(uiManager.viewController.view)
    addChild(uiManager.viewController)
    uiManager.viewController.view.translatesAutoresizingMaskIntoConstraints = false

    portraitConstraints = [
        uiManager.viewController.view.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0),
        uiManager.viewController.view.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0),
        uiManager.viewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),
        uiManager.viewController.view.heightAnchor.constraint(equalToConstant: min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * 9/16)
    ]

    landscapeConstraints = [
        uiManager.viewController.view.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0),
        uiManager.viewController.view.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0),
        uiManager.viewController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
        uiManager.viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
    ]

    portraitConstraints.forEach { $0.isActive = true }
}

Important

You must control display type yourself with playerManager?.uiManager?.config.displayType there is a difference in functionality provided for different display types. By default, UI is configured with DisplayType.regular(portrait) so you need to set it to .max if playback starts in a fullscreen mode

Default UI configuration (UIManagerConfig)

Selecting the default UI now gives you access to the UIManagerConfig API via playerManager.uiManager?.config. Use it to:

  • switch displayType manually when your app toggles orientations
  • override style and translations (fonts, colors, copy) that usually come from BackOffice
  • adjust seek button intervals for live/VOD (seekForwardIntervalLive, seekBackwardIntervalLive, etc.)
  • show/hide built-in elements through toggles (isCloseButtonVisible, isNowPlayingVisible, isDefaultPlaylistUiEnabled, isWhyThisAdIconEnabled, …)

Display Types

The SDK provides five distinct display types that control the UI layout and available functionality. Each display type is optimized for different use cases and screen sizes:

  • .naked – Minimal UI with no controls visible. Ideal for scenarios where you want to display only the video content without any overlay controls. This is useful for background playback or when you’re implementing completely custom controls.

  • .minibar – A compact horizontal control bar with essential playback controls but without video frames layer. Perfect for small embedded players while casting or for audio-only content. Provides basic play/pause, seek, and time display functionality.

  • .miniplayer – A compact player view designed for floating(above all content) player scenarios. Typically includes a small video window with minimal controls, suitable for multi-tasking or secondary content display.

  • .regular – The standard player UI optimized for portrait orientation and regular-sized views. This is the default display type and provides a full set of controls including play/pause, seek bar, settings, tracks selection, and more. Best suited for typical mobile viewing experiences.

  • .max – Fullscreen/maximized player UI optimized for landscape orientation and full-screen playback. Provides the most comprehensive set of controls and features, including enhanced seek functionality, full track selection UI, and all available player options. Use this when the player occupies the full screen or when in landscape mode.

Important: The display type affects which UI elements and functionality are available. For example, some features like advanced track selection or certain controls may only be available in .regular or .max modes. Always set the appropriate display type based on your app’s current orientation and player size to ensure the best user experience. For example you can rely on DorisViewTapEvent.fullScreenButtonTap event or DorisViewTapEvent.backButtonTap or DorisPlayerEvent.playbackReceiverChanged event to switch display types.

// Example: Switch display type based on orientation
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    coordinator.animate(alongsideTransition: { _ in
        if size.width > size.height {
            // Landscape - use fullscreen UI
            self.playerManager?.uiManager?.config.displayType = .max
        } else {
            // Portrait - use regular UI
            self.playerManager?.uiManager?.config.displayType = .regular
        }
    })
}

Custom UI

For custom UI, you can choose either to go with native AVPlayerViewController or AVPlayerLayer, both options will require additional Ads UI config that you should take care of based on Player Ads events if you want to support ads. You can rely on other player events to control you custom UI same way as described in Listen section

//AVPlayerViewController
vesperSDKManager.createPlayerManager(userInterfaceConfig: .custom(type: .avPlayerViewController(AVPlayerViewController()), adsConfig: nil))

//AVPlayerLayer
vesperSDKManager.createPlayerManager(userInterfaceConfig: .custom(type: .playerLayer(AVPlayerLayer()), adsConfig: nil))

Load a video

After you created playerManager and settled UI you can start playing content with it, you load a video like so:

import VesperSDK

let source = DorisResolvableSource(id: "123456", isLive: false)

playerManager.load(source: source) { error in
    if let error = error as? VesperSDKError {
        //handle load error
    }
}

Load a collection (playlist | season | watchlist)

If you want to load a video from some collection e.g. playlist season or watchlist you can specify it as an additional context parameter for DorisResolvableSource before loading, this will provide UI to access previous/next elements in a collection after video ends and optionally autotransition to the next episode

Note

This Only works with default UI

import VesperSDK

let watchContext = DorisWatchContext(type: .playlist, id: "<playlist_id>")
//        let watchContext = DorisWatchContext(type: .season, id: "<season_id>")
//        let watchContext = DorisWatchContext(type: .watchlist(ownerExternalId: "<owner_id>"), id: "<watchlist_id>")

let source = DorisResolvableSource(id: "123456", isLive: false, watchContextList: [watchContext])

playerManager.load(source: source) { error in
    if let error = error as? VesperSDKError {
        //handle load error
    }
}

When playing a collection you can access playerManager.queueManager to control next/previous items or access full list of items to implement custom UI for it

Take control

You can additionally control playback using playerManager api if needed

playerManager.play();
playerManager.pause()
playerManager.seek(.position(100))
playerManager.seek(.offset(5))
playerManager.seek(.date(Date.now)) //live only
playerManager.unload()

Listen

The SDK has a rich event-based system. events are separated to Player events (DorisPlayerOutputProtocol) and View events (DorisViewOutputProtocol) This is done by passing objects that implements those protocols to createPlayerManager call Refer to the Events guide for a complete list of callbacks and payloads, and to the States guide to understand the player/UI state machines mentioned below.

class ViewController: UIViewController {
    var vesperSDKManager: VesperSDKManager?

    override func viewDidLoad() {
        vesperSDKManager?.createPlayerManager(playerOutput: self, userInterfaceConfig: .default(output: self)) { result in }
    }
}

//playback events
extension ViewController: DorisPlayerOutputProtocol {
    func onSystemEvent(_ event: DorisSystemEvent) {}
    func onPlayerEvent(_ event: DorisPlayerEvent) {}
    func onPlayerStateChanged(old: DorisPlayerState, new: DorisPlayerState) {}
    func onAdvertisementEvent(_ event: DorisAdsEvent) {}
    func onRemoteCommandEvent(_ event: DorisRemoteCommandEvent) {}
}

//view events
extension ViewController: DorisViewOutputProtocol {
    func onViewUniversalEvent(_ event: DorisViewUniversalEvent) {}
    func onViewTapEvent(_ event: DorisViewTapEvent) {}
    func viewDidChangeState(old: DorisViewState, new: DorisViewState) {}
}

viewDidChangeState drives the default UI state machine documented in States, while player callbacks map to the state transitions described there as well.

Audio and Text Tracks

The ability to control the audio and text tracks is a key aspect of a playback experience.

Manual selection of tracks can be performed using the TracksManager which can be accessed via the PlayerManager like so:

playerManager.tracksManager.getTracks()
playerManager.tracksManager.selectTrack(type: .audio, identifier: .code("en"))
playerManager.tracksManager.selectTrack(type: .text, identifier: .title("English"))
playerManager.tracksManager.getSelectedAudioTrack()
playerManager.tracksManager.getSelectedSubtitleTrack()

You can listen to changes to audio and text tracks through DorisPlayerOutputProtocol event handler as described in the Listen section.

func onPlayerEvent(_ event: DorisPlayerEvent) {
    switch event {
    case .trackChanged(let track, let autoSet): break
    case .availableMediaTracksChanged(let tracks): break
    default: break
    }
}

Ads

The SDK comes with full Client-Side and Server-Side Ads support. The AdBreaksManager serves as your interface into the available ads during a playback session.

We can access it via the PlayerManager like so:

let currentlyActiveAdBreak = playerManager.adBreaksManager.activeAdBreak
let allAdBreaks = playerManager.adBreaksManager.adBreaks

Listen to ad related events with DorisPlayerOutputProtocol as described in the Listen section like so:

func onAdvertisementEvent(_ event: DorisAdsEvent) {
    switch event {
    default: break
    }
}

Note

If player is playing content that contains ads, player will emmit streamTime and contentTime separately you can also convert one into another using playerManager.adsManager if needed(e.g. for implementing custom UI)

Beacons

During playback, beacons allow us to periodically communicate with the server to ensure a user’s watch progress is updated. They also facilitate access-checks which ensure users still have access to the content, if not, playback error will be forced. Given they are a crucial component of the playback experience and can be tricky to get right, we’ve opted to take this weight of your shoulders.

As its largely a hands-off approach, the SDK provides a means to react to Beacon related errors.

These typically require user action and can be accessed on the SDK as described in the Listen like so:

func onPlayerEvent(_ event: DorisPlayerEvent) {
    switch event {
    case .playerItemFailed(let logs, let error):
        switch error {
        case .beacons(let beaconsError):
            switch beaconsError {
            case .accessForbidden: break
            case .conflictAnotherSession: break
            case .geoRestricted: break
            case .refreshTokenExpired: break
            }
        default: break
        }
    default: break
    }
}

Picture in Picture(PIP)

To make PIP work for your app you need to add corresponding Background Mode to you xCode project this will allow player to automatically switch to PIP mode when app is backgrounded

Note

after background mode is added you can control PiP with playerManager.config.toggles.isPipEnabled

playerManager.config.toggles.isPipEnabled = false // default is true

Chromecast

Add NSBonjourServices to your Info.plist SpecifyNSBonjourServicesin yourInfo.plist` to allow local network discovery to succeed on iOS 14.

You will need to add both _googlecast._tcp and _<cast_receiver_id>._googlecast._tcp as services for device discovery to work properly.

Vesper SDK will parse Info.plist and configure Google cast context

Update the following example NSBonjourServices definition and replace ABCD1234 with your cast receiver id.

<key>NSBonjourServices</key>
<array>
  <string>_googlecast._tcp</string>
  <string>_ABCD1234._googlecast._tcp</string>
</array>

Add NSLocalNetworkUsageDescription to your Info.plist We strongly recommend that you customize the message shown in the Local Network prompt by adding an app-specific permission string in your app’s Info.plist file for the NSLocalNetworkUsageDescription such as to describe Cast discovery and other discovery services, like DIAL.

<key>NSLocalNetworkUsageDescription</key>
<string>${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi
network.</string>
This message will appear as part of the iOS Local Network Access dialog as shown in the mock.

After this configurations are done casting with Vesper SDK is trivial using default UI. Once the player has been created and the content is loaded, all you need to do is tap on cast button in the right corner of player UI

Note

default UI contains built in Cast button to initiate cast session, if you use your own UI you might want to do extra steps to interact with Chromecast yourself following GoogleCast documentation https://developers.google.com/cast/docs/ios_sender/integrate