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
displayTypemanually when your app toggles orientations - override
styleandtranslations(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
.regularor.maxmodes. 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 onDorisViewTapEvent.fullScreenButtonTapevent orDorisViewTapEvent.backButtonTaporDorisPlayerEvent.playbackReceiverChangedevent 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