Skip to main content

Android SDK

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

Integration demo

A VXP GitHub account is required to gain access to:

Requirements​

Our SDK is designed to be used in modern Android applications with support for backward compatibility, up to devices running API 24(Android 7.0).

Kotlin is treated as our first class language in the SDK with 100% Java interoperability in mind.

Generated code is basically an Android library module which contains Kotlin code. But don't worry if your app is written in Java, as we always try to keep the experience and compatibility intact.

Gradle setup​

In order to fully integrate with DIVA features, the following third party dependencies are required:

    implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.7.10'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.13.0'
// chromecast
implementation 'com.google.android.gms:play-services-cast-framework:20.1.0'
implementation 'androidx.mediarouter:mediarouter:1.1.0'

DIVA uses a custom fork of Exoplayer in order to provide a set of patch fixes and also allow for multiple versions to exist within the same application if non DIVA playback is required by the hosting application.

In order to provide this we need to configure the hosting repository:

repositories {
def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties")))
...
maven {
url = uri("https://maven.pkg.github.com/deltatre-vxp/diva-android-exoplayer")
credentials {
username = githubProperties['github.usr']
password = githubProperties['github.key']
}
}
}

...
implementation 'com.deltatre.diva.media3:media3-exoplayer:1.1.1.0'
implementation 'com.deltatre.diva.media3:media3-exoplayer-ima:1.1.1.0'
....
implementation 'com.asha.vrlib:vrlib:2.4.1.1'

In order to use Diva Media Analytics Plugin you can link the plugins and third party libraries to work correctly, divacorelib is required for Media Analytics implementation.

More about Media Analytics integration can be found on Diva Integration Demo repo.

repositories {
def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties")))
...
maven {
url = uri("https://maven.pkg.github.com/deltatre-vxp/diva-android-releases")
credentials {
username = githubProperties['github.usr']
password = githubProperties['github.key']
}
...
maven {
url "https://npaw.jfrog.io/artifactory/youbora/"
}
}
...

//CONVIVA
implementation 'com.deltatre.diva:divaconvivaplugin:1.2.2'

//YOUBORA
implementation 'com.deltatre.diva:divayouboraplugin:1.2.2'

With a VXP GitHub account you can use package implementation by authorising your repository, i.e.

	repositories {
def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties")))
...
maven {
url = uri("https://maven.pkg.github.com/deltatre-vxp/diva-android-releases")
credentials {
username = githubProperties['github.usr']
password = githubProperties['github.key']
}
}


// diva mobile/tablet
implementation 'com.deltatre.diva:divaplayermobile:<latest-version>'

// diva tv
implementation 'com.deltatre.diva:divaplayertv:<latest-version>'

You will need to have a relevant Personal Access Token with package read permissions in order to utilise this approach.

Provisioning​

In order to initialize and use a DIVA player instance, we require two configurations and one provider.

The Settings, with which DIVA will be configured. The Dictionary, which will provide DIVA with the translations and localizations for the application. The VideoMetaDataProvider, which provides DIVA with information about the playback for a specific item and the ability to retrieve and refresh it.

Both Settings and Dictionary can optionally be implemented as Providers, or objects can be created locally for consumption by the API.

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 SettingClean, DictionaryClean and VideoMetaDataProvider objects respectively in order to work with the DIVA configuration and initialize the player objects.

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

Settings​

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"
},
"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}&amp;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": {
"startMode": "short",
"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"
}
}

Below is an example of a simple provider for settings retrieving data from a remote URL endpoint.

class SettingsProvider(var settingsBaseUrl: String,
override val coroutineContext: CoroutineContext = Dispatchers.Main): Serializable, CoroutineScope {
var settingResponseEvent: Event0? = null
var settingData: SettingClean? = null
private var jobSetting: Job = Job()
private var _this: SettingsProvider? = null

init {
settingResponseEvent = Event0()
_this = this
}

suspend fun requestSetting(settingId: String): SettingResponse {
val url = UrlUtils(settingsBaseUrl)
val endpoint = url.getFinallyUrl()
Logger.debug(String.format("Setting call url: %s, settingid: %s", endpoint, settingId))

val result = withContext(Dispatchers.IO) {
DataApiClient(endpoint).get<Setting>(settingId)
}

result?.let {
settingResponseEvent?.complete()
return SettingResponse.Response(it.toSettingClean())
}

return SettingResponse.Error("Setting Loading Error")
}

fun loadSetting(settingId: String?, callback: (SettingResponse) -> Unit) = runBlocking {
settingData = null
jobSetting.cancel()
if(settingId == null){
callback(SettingResponse.Error("Setting Id null"))
return@runBlocking
}
jobSetting = launch {
var response = _this?.requestSetting(settingId)
if(response == null){
response = SettingResponse.Error("Setting response null")
}
when(response) {
is SettingResponse.Error -> {
Log.d("Setting fetch", "Setting Loading Error")
}
is SettingResponse.Response -> {
settingData = response.settingData
}
}
callback(response)
}
}
}

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"

Below is an example of a simple provider for dictionary retrieving data from a remote URL endpoint.

class DictionaryProvider(
var dictionaryBaseUrl: String,
override val coroutineContext: CoroutineContext = Dispatchers.Main
) : Serializable, CoroutineScope {

var dictionaryResponseEvent: Event0? = null
var dictionaryData: DictionaryClean? = null
private var jobDictionary: Job = Job()

init {
dictionaryResponseEvent = Event0()
}

fun loadDictionary(
vocabularyEndpoint: String?,
callback: (DictionaryResponse) -> Unit
) = runBlocking {
dictionaryData = null
jobDictionary.cancel()
if (vocabularyEndpoint.isNullOrEmpty()) {
callback(DictionaryResponse.Error("Dictionary endpoint is empty"))
return@runBlocking
}

jobDictionary = launch {
val response = this@DictionaryProvider.requestVocabulary(vocabularyEndpoint)
when (response) {
is DictionaryResponse.Error -> {
Log.d(TAG, "Dictionary response is null")
}
is DictionaryResponse.Response -> {
dictionaryData = response.vocabularyData
}
}
callback(response)
}
}

suspend fun requestVocabulary(vocabularyEndpoint: String): DictionaryResponse {
val url = UrlUtils(dictionaryBaseUrl)
val endpoint = url.getFinallyUrl()

val result = withContext(Dispatchers.IO) {
DataApiClient(endpoint).get<Dictionary>(vocabularyEndpoint)
}

result?.let {
dictionaryResponseEvent?.complete()
return DictionaryResponse.Response(it.toDictionaryClean())
}

return DictionaryResponse.Error("Dictionary loading error")
}

companion object {
private const val TAG = "DictionaryProvider"
}

}

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

VideoMetaData​

In order to 'provide' VideoMetaData we must implement the VideoMetadataProviderInterface -

public interface VideoMetadataProviderInterface {
public abstract suspend fun requestVideoMetadata(videoId: kotlin.String, currentVideoMetadata: com.deltatre.divacorelib.models.VideoMetadataClean?, playbackState: com.deltatre.divacorelib.models.PlaybackState?): com.deltatre.divacorelib.providers.VideoMetadataResponse
}

A provider class would therefore resemble something like the following:

class VideoMetadataProvider(var videoMetadataBaseUrl: String) : VideoMetadataProviderInterface, Serializable {

override suspend fun requestVideoMetadata(videoId: String, currentVideoMetadata: VideoMetadataClean?, playbackState: PlaybackState?): VideoMetadataResponse {
val url = UrlUtils(videoMetadataBaseUrl)
val platform = if (playbackState?.chromecastMode == true) "chromecast" else "android"
val hdrType = if (playbackState?.hdrMode == true) "hdr10" else "none"
url.addParameter("platform", platform)
url.addParameter("hdrType", hdrType)
val endpoint = url.getFinallyUrl()
Log.d("DIVA", String.format("Videometadata call url: %s, videoid: %s, platform: %s, hdrType: %s", endpoint, videoId, platform, hdrType))

val result = withContext(Dispatchers.IO) {
DataApiClient(endpoint).get<VideoMetadata>(videoId)
}

result?.let {
return VideoMetadataResponse.Response(it.toVideoMetadataClean())
}

return VideoMetadataResponse.Error("Video Metadata Error")
}
}

For more information about Video Meta Data please click here.

Android Mobile​

Initialization​

Once the necessary data has been provisioned, a DIVA fragment is simple to instantiate.

val config = DivaConfiguration(
CONTEXT,
VIDEO_ID,
VIDEO_METADATA_PROVIDER,
SETTING,
DICTIONARY,
NETWORK_ERROR,
PARAMS,
DEEP_LINK_TYPE,
DEEP_LINK_VALUE,
PREFERRED_AUDIO_TRACK_NAME,
PREFERRED_CLOSED_CAPTION_TRACK_NAME,
BITRATE_PREFERENCES,
HDR_MODE,
DAI_IMA_AD_TAGS_PARAMETERS,
HIGHLIGHTS_MODE,
DIVA_LISTENER,
MEDIA_ANALYTICS_CALLBACK
)

DivaFragment.newInstance(config, PlayerMode.FULLSCREEN)

Android TV​

Leanback configuration​

DIVA Android TV uses elements of the Android leanback library but due to the specific functionality of DIVA, is unable to utilise most of the Leanback 'framework' and instead relies on resuse of specific framework components such as RowSupportFragment, VerticalGridView and so forth.

Therefore DO NOT create a default TV application when looking to integrate Android TV, and instead create a simple blank activity. Manifest elements in order to support Leanback are as follows:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.deltatre.diva.objectapiintegration">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>

<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="true" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:banner="@drawable/diva_banner"
android:theme="@style/Theme.Leanback">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

Without the launcher activity and the theme derived from Theme.Leanback the library will not run.

Initialization​

The UI for DIVA Android TV has been developed seperately from the DIVA mobile code largely due to the UI requirements and leanback integration. As such it utilises simple ViewModels which are not a feature of the mobile UI at this point. We will look to consolidate the interfaces in future builds.

Please note - also unlike the mobile build, DIVA Android TV uses Koin for DI. The library initialisation for Koin is done in the PlaybackVideoFragment, therefore the PlaybackVideoFragment must be initialised BEFORE viewmodels are created. We are open to review this and either provide explicit initialisation for Koin if necessary, this prevents the need for modifying or customising the Application class.

Simple instantiation code is as follows via onCreate:


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.player_main)

val playbackFragment = PlaybackVideoFragment.newInstance(applicationContext)

val assetDataProvider = AssetDataProvider(applicationContext)
divaPlaybackViewModel =
ViewModelProvider(this)[DivaPlaybackViewModel::class.java]

val setting = assetDataProvider.readJsonFromFile<Setting>(applicationContext, "basicSettings.json")?.toSettingClean()
val dictionary = assetDataProvider.readJsonFromFile<Dictionary>(applicationContext, "dictionaryEn_GB.json")
?.toDictionaryClean()

// Validate correct loading of settings and dictionary here

divaPlaybackViewModel.init(setting!!, dictionary!!, assetDataProvider)

lifecycleScope.launch {
divaPlaybackViewModel.videoMetaDataFlow.collect { videoMetaData ->
if (videoMetaData.status == NetworkResourceStatus.LOAD_FAILED || videoMetaData.data == null) {
// Apply error logic here
}
}
}

val conf = DivaConfiguration(
context = this,
videoId = "worldCupFinal",
setting = setting,
dictionary = dictionary)

divaPlaybackViewModel.load(conf)

supportFragmentManager.beginTransaction()
.replace(R.id.player_container, playbackFragment)
.commit()
}

AssetProvider here does what the above providers are doing, but from a simple JSON configuration from the components. However you want to instantiate the objects required by Diva and passed to the ViewModel init is up to you.

In order to listen to events being received by DIVA you can utilise the DivaListener as follows:


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.player_main)

val divaListener = object: DivaListener {
override fun onVideoError(error: VideoError, videoMetadata: VideoMetadataClean) {
Log.d(TAG,"DivaListener: On Video Error $error $videoMetadata")
}

override fun onAnalyticsCallback(event: AnalyticsEvent) {
Log.d(TAG, "DivaListener: On Analytics Event $event")
}

override fun onCustomActionResponse(payload: CustomActionPayload) {
Log.d(TAG,"DivaListener: On Custom Action Response $payload")
}

override fun onExit() {
Log.d(TAG,"DivaListener: On Exit")
}

override fun onAudioTrackSelected(track: String) {
Log.d(TAG,"DivaListener: On Audio Track Selected $track")
}

override fun onClosedCaptionTrackSelected(track: String) {
Log.d(TAG,"DivaListener: On Closed Caption Track Selected $track")
}

override fun onPlayerPosition(relativePosition: Long, absolutePosition: Date) {
Log.d(TAG,"DivaListener: On Player Position $relativePosition $absolutePosition" )
}
}

val playbackFragment = PlaybackVideoFragment.newInstance(applicationContext, divaListener)

val assetDataProvider = AssetDataProvider(applicationContext)
divaPlaybackViewModel =
ViewModelProvider(this)[DivaPlaybackViewModel::class.java]

val setting = assetDataProvider.readJsonFromFile<Setting>(applicationContext, "basicSettings.json")?.toSettingClean()
val dictionary = assetDataProvider.readJsonFromFile<Dictionary>(applicationContext, "dictionaryEn_GB.json")
?.toDictionaryClean()

// Validate correct loading of settings and dictionary here

divaPlaybackViewModel.init(setting!!, dictionary!!, assetDataProvider)

lifecycleScope.launch {
divaPlaybackViewModel.videoMetaDataFlow.collect { videoMetaData ->
if (videoMetaData.status == NetworkResourceStatus.LOAD_FAILED || videoMetaData.data == null) {
// Apply error logic here
}
}
}

val conf = DivaConfiguration(
context = this,
videoId = "worldCupFinal",
setting = setting,
dictionary = dictionary)

divaPlaybackViewModel.load(conf)

supportFragmentManager.beginTransaction()
.replace(R.id.player_container, playbackFragment)
.commit()
}

For any further event reporting requests please let the project team know.