Skip to main content

iOS/tvOS BO Adapter

DivaBOAdapter is a Diva player support plugin that collects and returns data needed for the player's configuration. The functionality of the plugin (in order) consists of:

  • Getting the Diva player settings
  • Getting the Diva player translations
  • Getting the video feed publisher data
  • Constructing the Diva Metadata object
  • Offering the videoMetadata call (needs to be extended in the integration project, see "# Extending the VideoMetadata Provider")
  • Offering the Entitlement Provider
  • Offering the Analytics Provider
  • Offering SSAI information
  • Offerring Google PAL SDK integration

DivaCore is a dependency of this plugin used to return Diva models.

Installation​

The plugin is available as a SPM package. Integrator needs to add DivaIOS and/or DivaTVOS spm repositories.

Diva iOS​

Plugin doesn't contain the Diva iOS Player wrapped inside as the other platforms. To avoid conflicts between the frameworks it is recommended both the adapter and the Diva SDK to be added as a SPM package. The packages needed are:

DivaIOS: url: https://github.com/deltatre-vxp/diva-ios-spm exactVersion: 5.9.0

Diva tvOS​

Use SPM to add Diva tvOS. DivaTVOS: url: https://github.com/deltatre-vxp/diva-tvos-spm exactVersion: 5.9.0

SPM configuration DivaBOAdapter: url: https://github.com/deltatre-vxp/diva-apple-bo-adapter-spm Up to Next Major: 6.0.0

Dependency Matrix​

ComponentVersionPackage Name
Diva player iOS5.9.0Diva.diva-ios-spm-aws
Diva player tvOS5.9.0Diva.diva-tvos-spm-aws
BO adapter6.0.0Diva.diva-apple-bo-adapter-spm-aws
Youbora2.7.0Diva.diva-apple-youbora-analytics-spm-aws
Conviva5.6.0Diva.diva-apple-conviva-analytics-spm-aws
GoogleInteractiveMediaAds3.10.0Diva.diva-apple-ima-spm-aws
DivaAdsCore2.4.0Diva.diva-apple-ads-core-spm-aws
DivaAdsBeaconing1.6.0Diva.diva-apple-ads-beaconing-spm-aws
DivaOMAnalytics1.6.0Diva.diva-apple-om-analytics-spm-aws
DivaGooglePalAnalytics3.4.0Diva.diva-apple-google-pal-spm-aws

Implementation​

After the adapter has been added, DivaBOAdapter needs to be imported in the class where the Diva player is configured. SessionId should be taken from divaBOResponse and used when DivaConfiguration is initialized. Sample configuration for an app not using async/await:

Task {
let divaBOAdapter = DivaBOWrapper()
do {
let boConfiguration = BOAdapterConfiguration(
countryCode: "en-US",
playerVersion: .init(
major: Diva.version.major,
minor: Diva.version.minor,
patch: Diva.version.patch
),
settingsUrl: "https://example.com/ios.json",
token: "123456",
videoID: videoID
)

let divaBOResponse = try await divaBOAdapter.getDivaData(boConfiguration)

self.entitlementProvider = divaBOResponse.entitlementProvider
let config = DivaConfiguration(
settings: divaBOResponse.divaSettings,
dictionary: divaBOResponse.translations,
videoId: videoID,
videoMetadataProvider: divaBOResponse.videoMetadataManager,
entitlementProvider: divaBOResponse.entitlementProvider,
customFairplayResourceLoader: CastlabsFairplayResourceLoader( // alternatively you can use AzureFairplayResourceLoader, we recommend CastlabsFairplayResourceLoader
fairplayCertificatePath: divaBOResponse.adapterSettings.entitlementCheck?.fairPlayCertificateUrl ?? ""
),
deepLinkType: .relative,
deepLinkValue: "\(position * 1000)",
sessionId: divaBOResponse.otherDivaConfiguration.sessionId,
onPlaybackSession: await divaBOResponse.entitlementProvider.getOnPlaybackSession()
)

self.setUpDiva(with: config) // Additional setup

} catch {
NSLog("divaBOAdapter Error: \(error)")
}
}

Data needed to initialize BO adapter is provided via BOAdapterConfiguration, more information about the parameters inside its definition. Note that some parameters are optional. In case you need to adjust the Entitlement Request before it is used by your entitlement service, you can use BOAdapterConfiguration.entitlementPayloadMap handler.

If the operation is successful the returned object is of type DIVABOResponse and consists of:

  • adapterSettings: AdapterSettings, needed for the construction of the VideoMetadataManager
  • divaSettings: SWSettingCleanModel, needed for the diva settings configuration
  • translations: SWDictionaryCleanModel, needed for the diva translation configuration
  • entitlementProvider: EntitlementProvider, needed for the entitlement. It also provides an handler onPlaybackSession for Diva.onPlaybackSession. It is needed for SSIA and improved analytics.
  • mediaAnalytics: Basic information to initialize both Conviva and/or Youbora analytics
  • videoMetadataManager: Initialized VideoMetadataManager which needs to be passed to DivaConfiguration
  • otherDivaConfiguration: Other Data needed to be passed to DivaConfiguration, e.g. sessionId

If the operation fails, function throws an error.

DIVABOResponse contains basic elements to initialize DivaConfiguration(iOS) and/or DivaTVConfiguration (tvOS) and then DivaPlayer.

Offline support has been added to the BO adapter, if you provide an URL which points to a local storage (URL.isFileURL == true), BO adapter will use it. It works for settings, translations and publisher. Alternatively you can use "{local.storage.path}" wildcard in the url and BO adapter will replace it in runtime. You can pass its value to BO adapter via BOAdapterConfiguration.extraParameters (e.g. ["local.storage.path" : "SOME-PATH"]) otherwise BO adapter will replace it with URL(fileURLWithPath: NSHomeDirectory()).absoluteString in runtime.

A sample url with wildcard would be {local.storage.path}/video/videodata/v2/{v.id}. For entitlement check we recommended passing to Diva Player a custom class which conforms to EntitlementProvider that can be used offline.

Extending the VideoMetadata Provider​

To provide extra flexibility integration project needs to extend the VideoMetadata Provider request method to use the adapter.

VideoMetadataManager.requestVideoMetadata(..) returns the VideoMetadata to be passed to Diva VideoMetadataProvider protocol. DRM provider ("v.source.drm.provider") and source origin ("v.source.origin") are returned inside SWVideoMetadataCleanModel.customAttributes, if empty string it means they were not available.

Below a sample extension for Diva iOS:

import DivaCore
import DivaIOS
import DivaBOAdapter

extension VideoMetadataManager: DivaCore.VideoMetadataProvider {
public func requestVideoMetadata(
videoId: String,
currentVideoMetadata: SWVideoMetadataCleanModel?,
playbackState: DivaModel.PlaybackState
) async throws -> SWVideoMetadataCleanModel {
do {
let videoMetadata = try await getVideoMetadata(
videoId: videoId,
isChromecastMode: playbackState.chromecastMode,
isAirPlayActive: playbackState.airplayActive)
return videoMetadata
} catch {
throw VideoMetadataError(
errorCode: "",
message: error.localizedDescription
)
}
}
}

Further customization​

Diva BO adapter provides the building blocks to initialize diva, this allows you to fine tune its initialization to achieve your specific flows.

For instance, to apply a resume point of 3 min, set a relative deep-link at 3 min.

var config = DivaConfiguration(...)
config.deepLinkType = .relative
config.deepLinkValue = "180000"

or if you would like to customize the video metadata returned by BO adapter to add certain parameter(s), for instance the Google Ima advertismeent tag. You can add it by modifying it before it gets passed to Diva in the VideoMetadataManager extension. Below a sample implementation for tvOS:

import DivaBOAdapter
import DivaCore
import DivaTVOS

extension VideoMetadataManager: @retroactive VideoMetadataProvider {
public func requestVideoMetadata(
videoId: String,
currentVideoMetadata: DivaCore.SWVideoMetadataCleanModel?,
playbackState: DivaCore.DivaModel.PlaybackState
) async throws -> DivaCore.SWVideoMetadataCleanModel {
do {
var videoMetadata = try await getVideoMetadata(
videoId: videoId,
isChromecastMode: false,
isAirPlayActive: false
)
let remoteAdUrl: String
if simpleWorkflow {
// simple workflow, use static url
remoteAdUrl = "https://hardcoded_ad_url"
} else {
// download url from remote server
remoteAdUrl = try await fetchAdUrl(videoId: videoId)
}
videoMetadata.ad = remoteAdUrl
return videoMetadata
} catch let error as DivaModel.VideoMetadataProviderError {
throw error
} catch let error as VideoMetadataManager.VideoMetadataManagerError {
throw DivaModel.VideoMetadataProviderError.other(message: error.description)
} catch {
throw error
}
}
}

Analytics​

BO Adapter retrieves basic information needed to initialize media analytics. Data is located in DIVABOResponse.mediaAnalytics. Currently Conviva, Youbora and Open Measurement are supported. Among different information provided, It informs whether the analytic is enabled or not. Furthermore to improve how you track video analytics, you can use DivaBOResponse.entitlementProvider.getPlaybackSessionId() which creates an ID per video session, e.g. you start watching a video and change to a new video in a video list, Diva will create a new id for it and BO adapter will process it.

For more information about media analytics see here.

Youbora​

Data located in DivaBOResponse.mediaAnalytics.youbora, data here is the basic data you need to provide to initialize Youbora analytics.

In your integration app, you need to add its SPM package, https://github.com/deltatre-vxp/diva-apple-youbora-analytics-spm

To pass data like app information, should be handled by integrator. Below a sample implementation of DivaIOS in an Axis based project:

import Axis_SDK
import Axis_API
import DivaBOAdapter
import DivaCore
import DivaIOS
import DivaYouboraAnalytics
import Foundation
import YouboraLib

class FullscreenDivaViewController: UIViewController {
....
var item: ItemSummary
var youboraAnalytics: DivaYouboraAnalytics.YouboraMediaAnalytics?
...

func launchDiva() async {
/// initialize Diva configuration, see Implementation section
//...
let playbackSessionId = await divaBOResponse.entitlementProvider.getPlaybackSessionId() // unique id that has been created each time player is started
await self.initializeYouboraAnalytics(settings: divaBOResponse.mediaAnalytics.youbora, item: self.item, playbackSessionID: playbackSessionId)
config.onMediaAnalyticsUpdate = { [weak self] mediaEvent in
guard let self else { return }
// contentCustomDimension1 is used to track playback session id
await self?.youboraAnalytics?.set(
optionValue: divaBOResponse.entitlementProvider.getPlaybackSessionId(),
path: \.contentCustomDimension1)
await self?.youboraAnalytics?.handle(mediaEvent)
}
// continue initializing Diva
//.....
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
youboraAnalytics = nil
}
}
extension FullscreenDivaViewController {
/// Initializes Youbora analytics with basic values
func initializeYouboraAnalytics(settings: DivaBOAdapter.YouboraMediaAnalytics, item: ItemSummary, playbackSessionID: String) async {
guard settings.enabled else { return }
let youboraOptions = YBOptions()
youboraOptions.accountCode = settings.accountCode
youboraOptions.contentCdn = settings.cdnName

youboraOptions.appName = AppInfo.CFBundleName
youboraOptions.appReleaseVersion = AppInfo.CFBundleShortVersionString
youboraOptions.username = app.accountManager.account?.id

youboraOptions.contentPlaybackType = item.type.rawValue
youboraOptions.contentChannel = item.channelShortCode
youboraOptions.program = item.showTitle
youboraOptions.contentTitle = item.title
youboraOptions.contentId = item.id
youboraOptions.contentCustomDimension1 = playbackSessionID

let ybAnalytics = await YouboraMediaAnalytics(
options: youboraOptions,
settings: .init(playerName: settings.playerName,
playerVersionFramework: Diva.version.framework(),
logLevel: YBLogLevel.verbose)
)
youboraAnalytics = ybAnalytics
}
}

Conviva​

Data located in DivaBOResponse.mediaAnalytics.conviva, data here is the basic data you need to provide to initialize Conviva analytics.

In your integration app, you need to add its SPM package, https://github.com/deltatre-vxp/diva-apple-conviva-analytics-spm.

For further steps on how to integrate it see https://divadocs.deltatre.net/docs/sdk/conviva-plugin/. Below a sample implementation of DivaIOS in an Axis based project:

import Axis_SDK
import Axis_API
import DivaBOAdapter
import DivaCore
import DivaIOS
import DivaYouboraAnalytics
import Foundation
import YouboraLib

class FullscreenDivaViewController: UIViewController {

func launchDiva() {
/// initialize Diva configuration, see Implementation section

config.onMediaAnalyticsUpdate = { [weak self] mediaEvent in
guard divaBOResponse.mediaAnalytics.conviva.enabled else { return }
await ConvivaAnalytics.shared.set(customDictionary: [
"playbackSessionId": await divaBOResponse.entitlementProvider.getPlaybackSessionId()
])
await ConvivaAnalytics.shared.handle(mediaEvent)
}

// continue initializing Diva
}
}

import DivaConvivaMediaAnalytics
import DivaIOS
import Foundation
import DivaCore
import DivaIOS

actor ConvivaAnalytics {
static let shared = ConvivaAnalytics()

private var customDictionary: [String:String] = [:]

func set(customDictionary: [String:String]) {
self.customDictionary = customDictionary
}

func handle(_ update: MediaAnalyticsUpdate) async {

if tracker == nil {
tracker = await createMediaAnalytics()
}
await tracker?.handle(update)
if update.action == .videoClosed {
tracker = nil
}
}

func reset() async {
tracker = nil
}

private (set) var tracker: ConvivaMediaAnalytics?

private func createMediaAnalytics() async -> ConvivaMediaAnalytics {
await ConvivaMediaAnalytics(
customerKey: "<INSERT-CONVIVA-CUSTOMERKEY-HERE>",
touchstoneURL: Diva.isDebugMode
? "<INSERT-DEBUG-TOUCHSTONE-HERE>"
: nil,
settings: .init(
cdnName: "<INSERT-CDN-NAME-HERE>",
playerName: "Diva",
viewerId: app.accountManager.account?.id ?? "anonymous",
playerVersionFramework: Diva.version.framework(),
applicationBuildVersion: (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? Diva.version.framework(),
tags: { [weak self] metadata in
guard let self = self else { return [:] }
let customMetadata = [
"platform": "iOS",
"playerVersion": Diva.version.framework(),
"videoId": metadata.videoId,
"videoTitle": metadata.title,
"eventId": metadata.eventId,
"is24_7": String(metadata.dvrType == ._none),
"is360": String(metadata.stereoMode != ._none),
"assetState": metadata.assetState.rawValue,
"spoilerMode": metadata.behaviour.spoilerMode.rawValue
]
return await customMetadata.mergeKeepCurrent(await self.customDictionary)
}
)
)
}
}

Open Measurement​

Data located in DivaBOResponse.mediaAnalytics.openMeasurement, data here is the basic data you need to provide to initialize open measurement (OM) analytics.

In your integration app, you need to add its SPM package, https://github.com/deltatre-vxp/diva-apple-om-analytics-spm

To pass data like app information, should be handled by integrator. Below a sample implementation of DivaIOS in an Axis based project:

import Axis_SDK
import Axis_API
import DivaBOAdapter
import DivaCore
import DivaTVOS
import DivaOMAnalytics

class FullscreenDivaViewController: UIViewController {
....
var item: ItemSummary
var omAnalytics: DivaOMAnalytics?
...

func launchDiva() async throws {
//..
let divaBOResponse = try await divaBOAdapter.getDivaData(... )
await createOmAnalytics(settings: divaBOResponse.mediaAnalytics.openMeasurement)

var config = await DivaTVConfiguration(... )

config.onAnalyticEvent = { [weak self] event in
await self?.omAnalytics?.handleAnalytics(event)
}

config.onMediaAnalyticsUpdate = { [weak self] mediaEvent in
await self?.omAnalytics?.handleMediaAnalytics(mediaEvent)
}
// continue initializing Diva
//.....
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
omAnalytics = nil
}
}
extension FullscreenDivaViewController {
/// Initializes OM analytics with basic values
func createOmAnalytics(settings: DivaBOAdapter.OpenMeasurementAnalytics?) async {
guard settings?.enabled ?? false else { return }
guard let partnerName = settings?.partnerName, !partnerName.isEmpty else { return }
let playerVersion = DivaTV.version.framework()
omAnalytics = await DivaOMAnalytics(
partnerName: partnerName,
playerVersion: playerVersion,
logLevel: .notice
)
}
}

Live to VOD​

In order to handle live to vod transition, Integrator needs to survey Video Metadata to allow Diva to notice the metadata change. There are 2 items which should be used to achieve this:

  1. BO adapter exposes a parameter which determines how often the survey needs to be done. Parameter is located in DIVABOResponse.adapterSettings.videoData?.videoDataPollingInterval and send in seconds.
  2. Diva provides a function, Diva.requestVideoMetadataUpdate(), which triggers Diva to ask VideoMetadataProvider for video metadata of the current video. With this task Diva gets to know if there is a video metadata type change, aka from live to vod
  3. If there is an Entitlement error, integrating app should cancel survey done in step 1/2. DIVABOResponse.entitlementProvider.errorPublisher provides an error if it occurs.

Below a sample implementation in DivaIOS:

import Combine
import DivaIOS

class FullscreenDivaViewController: UIViewController {
var diva: Diva?
var videoPoolingCancellable: AnyCancellable?
var entitlementProviderCancellable: AnyCancellable?
// ..... // code here

func launchDiva() {
/// initialize Diva configuration, see Implementation section
let boConfiguration = BOAdapterConfiguration(
countryCode: model.divaLaunchParams.dictionaryId,
extraParameters: ["Run.idfa": "00000000-0000-0000-0000-000000000000",
"Run.idfv": "4987117D-3844-4F87-8B83-8D174CBAFA34",
"Run.ppid": "ddd8daf23d722e7d182fdcf8346c4bc13bed72e4e627fcfe8068fb18b4807529"],
overriddenAudioTracksData: .init(
sourceOrigin: "harmonic",
tracks: [
.init(id: "eng", displayName: ""),
.init(id: "deu", displayName: "")
]
),
playerVersion: .init(
major: Diva.version.major,
minor: Diva.version.minor,
patch: Diva.version.patch
),
settingsUrl: model.divaLaunchParams.settingId,
token: "123456",
videoID: videoID,
palDelegate: divaPalDelegate
)

let divaBOResponse = try await divaBOAdapter.getDivaData(boConfiguration)

// Live to VOD pooling
if let pollingInterval = divaBOResponse.adapterSettings.videoData?.videoDataPollingInterval,
pollingInterval != 0 {
self.videoPoolingCancellable = Timer
.publish(every: TimeInterval(pollingInterval / 1000), on: .main, in: .common)
.autoconnect()
.sink { [weak self] date in
self?.diva?.requestVideoMetadataUpdate()
}
}

// Cancel video pooling if there is an entitlement error
self.entitlementProviderCancellable =
divaBOResponse.entitlementProvider.errorPublisher
.sink(receiveValue: { [weak self] error in
NSLog("entitlementProvider.errorPublisher: \(error)")
self?.videoPoolingCancellable?.cancel()
self?.videoPoolingCancellable = nil
})

// Continue with initialization
}
}

SSAI​

If you application implements Harmonic or AWS SSAI you can use the below plugins.

DivaAdsCore, DivaAdsBeaconing and DivaOMAnalytics are necessary when implementing both Harmonic and AWS SSAI. When implementing AWS we recommend using DivaGooglePalAnalytics as well. Confirm the set up with your video integration team.

DivaGooglePalAnalytics allows integrator to modify the NonceRequest which the plug in uses to request the nonce, see setPALNonce handler. To improve performance Google Pal allow us to specify if storage is allowed (used for TCFv2 compliance). Default value is false. Note that It is responsibility of the integrator to preserve the users consent response. Google provides an example on how you may want to preserve it using UserMessagingPlatform, see: https://developers.google.com/admob/ios/privacy/gdpr#how_to_read_consent_choices, https://iabeurope.eu/iab-europe-transparency-consent-framework-policies/#A_Purposes. In order to notify Google Pal about player activities, you need to pass Diva.api to it by using setDivaApi(_ divaApi: PlayerAPI), see the example below.

Below a sample implementation in DivaTVOS:

import DivaCore
import DivaGooglePalAnalytics
import DivaAdsBeaconing
import DivaBOAdapter
import DivaAdsCore
import DivaTVOS

var adsCorePlugin: DivaAdsCorePlugin?
var adBeaconingAnalytics: DivaAdvertisementBeaconing?

func setUpDivaPlayer() async throws {

let divaPalDelegate = await DivaPalDelegate(
logLevel: .debug,
playerVersion: DivaTV.version.framework(),
setPALNonce: { [weak self] nonceRequest in
// add or modify some extra parameters like ppid.
// below are sample parameters, check with your video integration team for the values.
// more information about what can be modified here: https://developers.google.com/ad-manager/pal/ios
nonceRequest.willAdAutoPlay = .on
nonceRequest.willAdPlayMuted = .off
nonceRequest.continuousPlayback = .on
nonceRequest.isIconsSupported = true
return nonceRequest
}
)

var divaBOAdapter = DivaBOWrapper(logLevel: .info)

let boConfiguration = BOAdapterConfiguration(
countryCode: "en-US",
playerVersion: .init(
major: Diva.version.major,
minor: Diva.version.minor,
patch: Diva.version.patch
),
settingsUrl: "https://example.com/apple.json",
token: "123456",
videoID: videoID,
palDelegate: divaPalDelegate
)

let divaBOResponse = try await divaBOAdapter.getDivaData(boConfiguration)

var config = DivaTVConfiguration( .. )

await createOmAnalytics(settings: divaBOResponse.mediaAnalytics.openMeasurement) // See above for sample

config.onMediaAnalyticsUpdate = { [weak self] mediaEvent in
await self?.omAnalytics?.handleMediaAnalytics(mediaEvent)
await self?.adBeaconingAnalytics?.handleMediaAnalytics(mediaEvent)
// add other analytics if needed, e.g. conviva or youbora
}

let initializedDiva = await DivaTV(configuration: config)

if let ssaiSetting = divaBOResponse.adapterSettings.ssai {
let entitlementResponsePublisher = await divaBOResponse.entitlementProvider.getEntitlementResponsePublisher()
adsCorePlugin = await DivaAdsCorePlugin(
ssaiSettings: ssaiSetting,
entitlementResponsePublisher: entitlementResponsePublisher,
api: initializedDiva.api,
logLevel: .notice,
detectionBuffer: 0
)
if divaBOResponse.mediaAnalytics.adsBeaconing?.enabled ?? false {
adBeaconingAnalytics = DivaAdvertisementBeaconing()
}
if ssaiSetting.enableGooglePAL {
await divaPalDelegate.setDivaApi(initializedDiva.api)
}
} else {
print("divaBOResponse.adapterSettings.ssai was nil, unable to initialize DivaAdsCorePlugin")
}
//... continue with initialization
}

Troubleshoot​

If you encounter a 403 error while trying to download Diva dependancies via github, it might be because of the github token not being passed. There are several ways for this to be solved:

The easiest way would be the second option, which is adding a "~/.netrc" file in your user directory (if its not already there) with the following content:

machine api.github.com
login name-surname-deltatre
password ghp_access-token

machine github.com
login name-surname-deltatre
password ghp_access-token

and replace the name-surname-deltatre and ghp_access-token with your github account and your github personal access token

There may be occasions where SPM gets mismatched builds. In this case you can clear SPM cache from your computer: There may be occasions where SPM gets mismatched builds. In this case you can clear SPM cache from your computer:

rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf ~/Library/org.swift.swiftpm

Remove the Xcode's derived data from your project, replace {your project} with your projects name

~/Library/Developer/Xcode/DerivedData/<your project>

Then, in Xcode, execute

File-->Swift Packages-->Reset Package Caches