Skip to main content

iOS SDK

This guide explains how to integrate DIVA into your iOS application.

Integration demo

A VXP GitHub account is required to gain access to:

Requirements​

DIVA requires a minimum of Xcode 15. Please ensure these are installed on your system before attempting to integrate DIVA

Installation with SPM​

If you are using SPM, you should add the following to your Package.swift file:

.package(url: "https://github.com/deltatre-vxp/diva-ios-spm.git", from: "5.8.3")

Additional steps to configure Google Cast​

Diva depends on Google Cast to offer you its features. Google requires host apps to follow extra steps. Steps are described here. For step (1) Add the Cast iOS SDK, please use the link provided above if you use cocoapods, SPM will take of it. Integrator needs to take care of steps 2 to 4.

Provisioning​

In order to initialize and use a DIVA player instance, we require the provision of three configuration objects.

  • Settings, with which DIVA will be configured.
  • Dictionary, which will provide DIVA with the translations and localisations for the application.
  • VideoMetaDataProvider, which provides DIVA with information about the playback for a specific item.

For more information about these items, please consult the Documentation.

This data can be provided as the application developer requires, but will need to return SWSettingCleanModel and SWDictionaryCleanModel (initializers which converts SWSettingModel and SWDictionaryModel into its "Clean" conterpart are provided in 'DivaCore) and a provider that implements the VideoMetadataProvider interface in order to work with the DIVA configuration and initialise the player objects.

Examples of provision for each of these items are as follows:

Settings​

Settings can be retrieved via remote endpoints or local configuration, it is up to the developer and integrating project.

An example of settings in json format is as follows:

{
"general": {
"audioSelectionMethod": "lang",
"closedCaptionSelectionMethod": "lang",
"expectedLiveDuration": 3600000,
"increaseLiveDuration": 600000,
"isMiddleTimelineEventsLineEnabled": true,
"isTimelineEventsVisibleWithCommentaryOpen": false,
"isVideoThumbnailPreviewEnabled": true,
"jumpLargeGaps": true,
"liveBackOff": 30000,
"minimalLayoutWidth": 600,
"relevantCommentaryStartsVisible": false,
"resolveManifestUrl": true,
"smallGapLimit": 0,
"trackVideoDataManifest": false,
"videoAnalyticsEventFrequency": 60000,
"culture": "en-GB"
},
"colours": {
"base2": "#0071F9",
"settingBtnBg": "#0071F9",
"overlayHighlightFg": "#0071F9",
"overlayBackgroundColor": "#FF0000",
"overlayTextColor": "#00FF00",
"overlayHighlightBackgroundColor": "#0000FF"
},
"customPlayByPlay": [
{
"key": "FirstExtraTimeEnd_Big",
"value": "https://divademo.deltatre.net/overlayassets/imgml/diva/icons/{p.density}/phase_end_big.png"
},
{
"key": "FirstExtraTimeEnd_Mini",
"value": "https://divademo.deltatre.net/overlayassets/imgml/diva/icons/{p.density}/phase_end_small.png"
},
{
"key": "FirstExtraTimeStart_Big",
"value": "https://divademo.deltatre.net/overlayassets/imgml/diva/icons/{p.density}/phase_start_big.png"
}
],
"ecommerce": {
"feedUrl": "https://divademo.deltatre.net/DIVAProduct/www/HTML5/DIVA/ecommerce/index.html?eventId={v.eventId}&culture={d.culture}",
"wordTag": "shop",
"ecommerceId": "e-commerce",
"iconUrl": "https://divademo.deltatre.net/DIVAProduct/www/Data/Diva5.0Test/img/shop_icon_white.png",
"toleranceWindow": 5000,
"showNotificationsOnce": false
},
"highlights": {
"shortFilter": ["GOAL", "OwnGoal"],
"mediumFilter": ["Goal", "OwnGoal", "PenaltyGoal", "YellowCard", "RedCard"],
"longFilter": ["*"],
"liveFilter": [
"Goal",
"OwnGoal",
"PenaltyGoal",
"YellowCard",
"RedCard",
"Substitution"
]
},
"multicam": {
"fallbackImage": "https://divademo.deltatre.net/overlayassets/imgml/diva/{cam.id}.jpg",
"fieldConfigUrl": "https://divademo.deltatre.net/DIVAProduct/www/Data/Diva5.0Test/output/multicam/field/field.json",
"videoListUrl": "https://divademo.deltatre.net/DIVAProduct/www/Data/DivaDemoIBC/multicam/{n:cam.VideoRef}_multiangle.xml"
},
"pushEngine": {
"configUrl": "https://divademo.deltatre.net/DIVAProduct/www/Data/DivaDemoIBC/PushEngine/pushengineConfig_HBS.json",
"eCommerceCollectionName": "eCommerceDemo",
"editorialCollectionName": " "
},
"syncDataPanels": {
"dataFolderUrl": "https://divademo.deltatre.net/DIVAProduct/www/Data/Diva5.0Test/output/OverlayLiteData/{V.EventId}.{d.Culture}/{OverlayID}.xml",
"renderingFolderUrl": "https://divademo.deltatre.net/DIVAProduct/www/Data/Diva5.0Test/output/RenderingLiteData{n:ResourceURI}",
"trustedOrigins": "https://divadoc.deltatre.net,https://divademo.deltatre.net"
},
"videoCast": {
"castBackground": "https://divademo.deltatre.net/DIVAProduct/www/Data/Diva5.0Test/img/diva_chromecast.jpg",
"chromecastAppID": "3282D6DE"
}
}

This is a more complex settings example that could be configured to load and parse, at the most simple level a minimal settings could be created with the following line of code:

    // A basic settings example
static let settings_minimal_example = SWSettingCleanModel.make(general:SWGeneralCleanModel.make(culture: "en-GB"))

For more information about settings and the available options click here.

Dictionary​

Dictionary must be provided based on the locale and will resemble something like the following:

{
"messages": {
"diva_go_live": "Go Live",
"diva_video_error": "This video is not working or not available in your region.",
"diva_error_button_ok": "OK",
"diva_menu_full_stats_button": "All Stats",
"diva_playbutton": "Play",
"diva_pausebutton": "Pause"

Simple construction can be done in Swift as follows:

    static let dictionary_example = SWDictionaryCleanModel(messages: [
"diva_live":"Live Now",
"diva_go_live":"Go Live",
"diva_video_error":"This video is not working or not available in your region.",
"diva_error_button_ok":"OK",

For more information about dictionary and the available options click here.

VideoMetaData​

For the VideoMetaData we must implement the provider interface to be passed to our configuration. This will allow DIVA to request provision of data as and when needed based on the Video ID.

public protocol VideoMetadataProvider {
func requestVideoMetadata(
videoId: String,
currentVideoMetadata: VideoMetadata?,
playbackState: PlaybackState
) async throws -> VideoMetadata
}

There is no limitation as to how this provision can be implemented, in a live environment a remote endpoint would be obviously required, but a simple hard coded conversion of VideoMetaData is as follows:

extension ExampleVideoMetadataProvider: VideoMetadataProvider {
func requestVideoMetadata(
videoId: String,
currentVideoMetadata: VideoMetadata?,
playbackState: PlaybackState
) async throws -> VideoMetadata
try generateVideoMetadataResponse(videoId: videoId)
}
}
....

private func generateVideoMetadataResponse(videoId: String) throws -> VideoMetadata {
switch videoId {
case ExampleConfiguration.videoId_fravscro:
return fravscro
....
default:
throw VideoMetadataError(errorCode: "404", message: "Video Metadata Error")
}
}

....

private let fravscro = SWVideoMetadataCleanModel.make(
videoId: "c6455bff-945f-42b2-af99-81dcd5aeba29",
title: "France Vs Croatia",
image: "https://fwc2018-vod-hbs.akamaized.net/1AD7F2771FD234212902111890CA17EC/1AD7F2771FD234212902111890CA17EC.jpg",
eventId: "108606",
programDateTime: "2018-07-15T13:40:15.681Z",
trimIn: 4680858,
trimOut: 11641166,
assetState: .vod,
ad: "https://divademo.deltatre.net/DIVAProduct/www/Data/Vast/skippable2.xml",
sources: [
SWVideoSourceCleanModel.make(
uri: "https://vod-ffwddevamsmediaservice.streaming.mediaservices.windows.net/6e8b64ef-feb3-4ea9-9c63-32ef7b45e1dd/6e8b64ef-feb3-4ea9-9c63-32ef7b45.ism/manifest(format=m3u8-aapl,filter=hls)",
format: "HLS")
],
audioTracks: [SWAudioTrackCleanModel(_id: "English1" , selector: "English", label: "English")],
defaultAudioTrackId: "English1",
vttThumbnails: .make(baseUrl: "https://divademo.deltatre.net/thumbnails/", filename: "thumbnails.vtt")
dvrType: .full)

Initialization​

Once the necessary data has been provisioned, a DIVA object is simple to instantiate, through the creation of a DivaConfguration with the relevant Settings, Dictionary and VideoMetaData provision.

Below is a sample implementation:

   override func viewDidLoad() {
super.viewDidLoad()

var config = DivaConfiguration(
settings: ExampleConfiguration.settings_demo_example,
dictionary: ExampleConfiguration.dictionary_example,
videoId: ExampleConfiguration.videoId_fravscro,
videoMetadataProvider: ExampleVideoMetadataProvider(),
logLevel: ExampleConfiguration.enableLogs ? .debug : .notice,
embedMode: .chromeless(loopback: true)
)
config.settingsError = "Settings Error"
config.networkError = "Network Error"
config.onVideoError = { error, videoMetadata in
print("[VideoError] VideoError received for video: \(videoMetadata?.videoId)")
print("[VideoError] error: \(error.type.rawValue) message: \(error.message)")
}
config.onAnalyticEvent = { print($0) }
config.onMediaAnalyticsUpdate = { ConvivaAnalytics.shared.handle($0) }
config.onExit = { [weak self] in
self?.dismiss(animated: true)
}

Task { @MainActor [weak self] in
guard let self else { return }
self.diva = await Diva(configuration: config)
self.diva?.setTargetView(view: self.view, viewController: self)
}
}

As a container​

If you need to switch Diva videos (for example, different episodes), you can wrap a Diva view controller into a container view controller:

class CustomContainerViewController: UIViewController {
private var divaContainerViewController: UIViewController?

func addDiva(viewController: UIViewController) {
// 1. Call the addChild(_:) method of your container view controller to configure the containment relationship.
addChild(viewController)

// 2. Add the child’s root view to your container’s view hierarchy.
view.addSubview(viewController.view)

// 3. Add constraints to set the size and position of the child’s root view.
constraints = [
viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
viewController.view.topAnchor.constraint(equalTo: view.topAnchor),
viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]
NSLayoutConstraint.activate(constraints)

// 4. Call the didMove(toParent:) method of the child view controller to notify it that the transition is complete.
viewController.didMove(toParent: self)
divaContainerViewController = viewController
}
}

Then wrap it into another container:

class FullScreenWithContainerViewController: UIViewController {
var divaContainer: CustomContainerViewController?

override func viewDidLoad() {
super.viewDidLoad()

setUpDivaContainer()
setUpNextEpisodeButton()
initializeDiva()
}

func setUpNextEpisodeButton() {
presentationModeButton.addTarget(self, action: #selector(initializeDiva),
for: .primaryActionTriggered)
presentationModeButton.setTitle("Next Video", for: .normal)
presentationModeButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(presentationModeButton)
NSLayoutConstraint.activate([
presentationModeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
presentationModeButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}

func setUpDivaContainer() {
let viewController = CustomContainerViewController()

// 1. Call the addChild(_:) method of your container view controller to configure the containment relationship.
addChild(viewController)

// 2. Add the child’s root view to your container’s view hierarchy.
view.addSubview(viewController.view)

// 3. Add constraints to set the size and position of the child’s root view.
let constraints = [
viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
viewController.view.topAnchor.constraint(equalTo: view.topAnchor),
viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]
NSLayoutConstraint.activate(constraints)

// 4. Call the didMove(toParent:) method of the child view controller to notify it that the transition is complete.
viewController.didMove(toParent: self)
divaContainer = viewController
}

@IBAction func initializeDiva() {
let vc = FullscreenDivaViewController()
self.divaContainer?.addDiva(viewController: vc)
}
}

And then use FullScreenWithContainerViewController instead of FullscreenDivaViewController

DivaMultivideo​

Multivideo mode is also supported on iOS

    override func viewDidLoad() {
super.viewDidLoad()

var config1 = DivaConfiguration(
settings: ExampleConfiguration.settings_demo_example,
dictionary: ExampleConfiguration.dictionary_example,
videoId: ExampleConfiguration.videoId_fravscro,
videoMetadataProvider: ExampleVideoMetadataProvider(),
logLevel: ExampleConfiguration.enableLogs ? .debug : .notice,
embedMode: .chromeless(loopback: true)
)
config1.onExit = {
Task { @MainActor [weak self] in
await self?.dismissAsync(animated: true) // You must dismiss container controller on exit
}
}
var config2 = DivaConfiguration(
settings: ExampleConfiguration.settings_demo_example,
dictionary: ExampleConfiguration.dictionary_example,
videoId: ExampleConfiguration.another_videoId,
videoMetadataProvider: ExampleVideoMetadataProvider(),
logLevel: ExampleConfiguration.enableLogs ? .debug : .notice,
embedMode: .chromeless(loopback: false)
)
config2.onExit = {
Task { @MainActor [weak self] in
await self?.dismissAsync(animated: true) // You must dismiss container controller on exit
}
}
diva = DivaMultivideo(config: [config1, config2]])
diva?.setTargetView(view: self.view, viewController:self)

DivaConfiguration fields​

logLevel​

Logger messages minimal level. Available values are debug, info, notice (default), error and fault. DivaLogger uses Apple's logging underneath which allows you to log some levels into device and recover it if needed.

embedMode​

You case use the followign values

  • fullscreen - if you want to display Diva player on the whole device screen.
  • embedded - deprecated, please don't use it for now.
  • chromeless(loopback: Bool) - if you want to embed Diva player into a view as a subview. No player controls are visible. Set loopback to true if you want to restart playback on end.

DivaLogger​

Besides Diva configuration, DivaLogger is also present in plugins. For instance Conviva plug in uses DivaLogger to log events:

public actor ConvivaMediaAnalytics {
public private(set) var currentSettings: Settings
let logger: DivaLogger

public init(settings: Settings) {
currentSettings = settings
logger = DivaLogger(
subsystem: .conviva,
category: String(describing: Self.self),
level: settings.logLevel
)
logger.log("[DIVA-CONVIVA PLUGIN] - Plugin created and CISAnalyticsCreator initialized", type: .info)
}
}

API​

After you have initialized Diva, Diva exposes an interface to communicate which can alter its behaviour in runtime. Access it via Diva/api, this api conforms with DivaAPI protocol.

Remember that other way to customize Diva player is via SWVideoMetadataCleanModel. It is passed via VideoMetadataProvider.

DIVA API reference list:

/// Interface to communicate with Diva Player
public protocol PlayerAPI: AnyObject {
/// Timeline icon threshold consider to group colliding icons. Grouped icons are displayed in Timeline and highlights.
/// - Parameter distance: Value is express in points. A valid value is a positive number greater than or equal to 1. If 0, no grouping is applied. If negative value then default is applied.
func setTimelineIconsMinimalDistance(distance: Int) async

/// Sends Diva Player a command
/// - Parameter command: command to execute
func sendPlayerCommand(_ command: PlayerApiModel.Command) async

/// Returns DIVA session. It represent the lifecycle of a DIVA player instance.
/// Every time a DIVA player is instantiated, a session id is created, and discarded once DIVA player is removed from memory.
/// - Returns: String with the Diva session identifier
func getSessionId() async -> String

/// Returns current player state
/// - Returns: Player state
func getPlayerState() async -> PlayerApiModel.State
/// Returns a publisher with the current player state, initial state stoped
/// - Returns: publisher
func getPlayerStatePublisher() async -> AnyPublisher<PlayerApiModel.State, Never>

/// Returns current player position
/// - Returns: current player position
func getPlayerPosition() async -> PlayerApiModel.Position
/// Returns a publisher which returns an updated value of the position
/// - Returns: Publisher
func getPlayerPositionPublisher() async -> AnyPublisher<PlayerApiModel.Position, Never>

/// Returns the current Video metadata
/// - Returns: current video metadata, can be nil in case of error or before it arrives for the first time
func getVideoMetadata() async -> SWVideoMetadataCleanModel?
/// Publisher which returns the current video metadata
/// - Returns: publisher
func getVideoMetadataPublisher() async -> AnyPublisher<SWVideoMetadataCleanModel?, Never>
/// Publisher which returns the current media duration
/// - Returns: publisher
func getMediaDurationPublisher() async -> AnyPublisher<Double?, Never>

/// Sends a media analytical action to the player
/// - Parameter MediaAnalyticAction: Analytical action
/// Diva uses this value to send a media analytical event via ``DivaConfiguration.onMediaAnalyticsUpdate`` for iOS or ``DivaTVConfiguration.onMediaAnalyticsUpdate`` for tvOS.
/// This value does not alter Diva's behavior.
func sendEvent(mediaAnalyticAction: MediaAnalyticsUpdate.Action) async

/// Real time when video started.
var videoAbsoluteStartTime: Date? { get async }

/// Is player currently seeking to a new position or not.
var isSeeking: Bool { get async }

/// Video player container size
var playerSize: CGSize { get async }
}

public enum PlayerApiModel {
/// List of Commands which can be passed to Diva
public enum Command: Equatable, Sendable, CustomStringConvertible {
/// Mutes current video
case mute(value: Bool)
/// Pauses current video
case pause
/// Plays current video
case play
/// Changes playback rate
case playbackRate(value: Float)
/// Seek to a relative position
case seek(value: Double)
/// Seek to an specific date, takes into account ``SWVideoMetadataCleanModel.programDateTime``
case seekAbsolute(value: Date)
/// Changes the audio playback volume for the player.
/// A value of 0.0 indicates silence; a value of 1.0 (the default) indicates full audio volume for the player instance.
/// This property is used to control the player audio volume relative to the system volume.
case volume(value: Float)

public var description: String {
switch self {
case .play: return "play"
case .pause: return "pause"
case .mute(let value): return "mute: \(value)"
case .seek(let value): return "seek: \(value)"
case .seekAbsolute(let value): return "seekAbsolute: \(value)"
case .playbackRate(let value): return "playbackRate: \(value)"
case .volume(let value): return "volume: \(value)"
}
}
}

/// Diva Player state
public enum State: Equatable, Sendable, CustomStringConvertible {
/// Player is not initialized yet or is about to close
case stopped
// Playback is paused
case paused
// Playing content
case playing
// Playback has failed
case playbackFailed

public var description: String {
switch self {
case .stopped: return "stopped"
case .paused: return "paused"
case .playing: return "playing"
case .playbackFailed: return "playback failed"
}
}
}

/// Returns current player position
public struct Position: Equatable, Sendable {
/// Relative time in milliseconds. Takes in consideration trim-in value set in ``SWVideoMetadataCleanModel.trimIn``
public var milliseconds: Double
/// Current time expressed as Date, calculated based on ``SWVideoMetadataCleanModel.programDateTime``
public var absolute: Date?

public init(milliseconds: TimeInterval, absolute date: Date? = nil) {
self.milliseconds = milliseconds
absolute = date
}
}
}

public protocol DivaAPI: PlayerAPI {
var watchTogetherMode: WatchTogetherMode { get }
var watchTogetherModePublisher: AnyPublisher<WatchTogetherMode, Never> { get }

@available(swift, deprecated: 0.0.1, message: "Deprecated in 4.0.0, use the one in PlayerAPI")
var playerState: PlayerApiModel.State { get }
@available(swift, deprecated: 0.0.1, message: "Deprecated in 4.0.0, use the one in PlayerAPI")
var playerStatePublisher: AnyPublisher<PlayerApiModel.State, Never> { get }

@available(swift, deprecated: 0.0.1, message: "Deprecated in 4.0.0, use the one in PlayerAPI")
var playerPosition: PlayerApiModel.Position { get }
@available(swift, deprecated: 0.0.1, message: "Deprecated in 4.0.0, use the one in PlayerAPI")
var playerPositionPublisher: AnyPublisher<PlayerApiModel.Position, Never> { get }

var mediaDuration: Double? { get }
var mediaDurationPublisher: AnyPublisher<Double?, Never> { get }

var playbackRate: Float { get }
var playbackRatePublisher: AnyPublisher<Float, Never> { get }

var isPlaybackLive: Bool { get }
var isPlaybackLivePublisher: AnyPublisher<Bool, Never> { get }

var videoShown: Bool { get }
var videoShownPublisher: AnyPublisher<Bool, Never> { get }

@available(swift, deprecated: 0.0.1, message: "Deprecated in 4.0.0, use the one in PlayerAPI")
var videoMetadata: SWVideoMetadataCleanModel? { get }
@available(swift, deprecated: 0.0.1, message: "Deprecated in 4.0.0, use the one in PlayerAPI")
var videoMetadataPublisher: AnyPublisher<SWVideoMetadataCleanModel?, Never> { get }

var resolver: Resolver { get }

var playerActionRequestPublisher: AnyPublisher<DivaPlayer.Action, Never> { get }
}

public enum DivaPlayer {
public enum Action {
case play
case pause
case seek(PlayerApiModel.Position)
}
}

extension DivaPlayer.Action: CustomStringConvertible {
public var description: String {
switch self {
case .play: return "play"
case .pause: return "pause"
case let .seek(value): return "seek: \(value.milliseconds)s [\(String(describing: value.absolute))]"
}
}
}

public enum WatchTogetherMode {
case disabled
case enabled
case active
}
info
  • In order to start receiving Media Analytic events, Diva.mediaAnalyticsEnabled needs to be set to true, false is the default value.
  • Set Diva.isDebugMode to true to an extra debug layout over Chromecast receiver and extra logs.