diff --git a/app/.gitignore b/app/.gitignore
index 42afabfd..796b96d1 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1 +1 @@
-/build
\ No newline at end of file
+/build
diff --git a/app/build.gradle b/app/build.gradle
index 405b4e34..ee644b20 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -73,4 +73,8 @@ dependencies {
implementation libs.yospace
implementation project(':connectors:uplynk')
+
+ implementation fileTree(dir: './libs/AdScriptApiClient_v1.0.10.aar', include: ['*.aar', '*.jar'], exclude: [])
+ implementation project(':connectors:analytics:adscript')
+
}
diff --git a/app/libs/.gitignore b/app/libs/.gitignore
new file mode 100644
index 00000000..6d81fcdb
--- /dev/null
+++ b/app/libs/.gitignore
@@ -0,0 +1 @@
+*.aar
diff --git a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt
index 29778b9e..d4cc5ec2 100644
--- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt
+++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
+import com.nad.adscriptapiclient.AdScriptDataObject
import com.theoplayer.android.api.THEOplayerConfig
import com.theoplayer.android.api.THEOplayerView
import com.theoplayer.android.api.ads.LinearAd
@@ -44,6 +45,8 @@ import com.theoplayer.android.api.event.ads.AdsEventTypes
import com.theoplayer.android.api.event.ads.SingleAdEvent
import com.theoplayer.android.api.source.PlaybackPipeline
import com.theoplayer.android.api.source.SourceDescription
+import com.theoplayer.android.connector.analytics.adscript.AdscriptConfiguration
+import com.theoplayer.android.connector.analytics.adscript.AdscriptConnector
import com.theoplayer.android.connector.analytics.comscore.ComscoreConfiguration
import com.theoplayer.android.connector.analytics.comscore.ComscoreConnector
import com.theoplayer.android.connector.analytics.comscore.ComscoreMediaType
@@ -73,6 +76,7 @@ class MainActivity : ComponentActivity() {
private lateinit var nielsenConnector: NielsenConnector
private lateinit var comscoreConnector: ComscoreConnector
private lateinit var yospaceConnector: YospaceConnector
+ private lateinit var adscriptConnector: AdscriptConnector
private lateinit var uplynkConnector: UplynkConnector
override fun onCreate(savedInstanceState: Bundle?) {
@@ -83,6 +87,7 @@ class MainActivity : ComponentActivity() {
setupConviva()
setupComscore()
setupNielsen()
+ setupAdscript()
setupYospace()
setupUplynk()
setupAdListeners()
@@ -189,6 +194,26 @@ class MainActivity : ComponentActivity() {
nielsenConnector = NielsenConnector(applicationContext, theoplayerView.player, appId, true)
}
+ private fun setupAdscript() {
+ val config = AdscriptConfiguration(implementationId = "exampleadscript", debug = true)
+ val metadata = AdScriptDataObject().apply {
+ set(AdScriptDataObject.FIELD_assetId, "bbb-example")
+ set(AdScriptDataObject.FIELD_type, AdScriptDataObject.OBJ_TYPE_content)
+ set(AdScriptDataObject.FIELD_program, "animation")
+ set(AdScriptDataObject.FIELD_title, "Big Buck Bunny")
+ set(AdScriptDataObject.FIELD_crossId, "1234")
+ set(AdScriptDataObject.FIELD_length, "596000")
+ set(AdScriptDataObject.FIELD_livestream, "0")
+ set(AdScriptDataObject.FIELD_attribute, AdScriptDataObject.ATTRIBUTE_RegularProgram)
+ }
+ adscriptConnector = AdscriptConnector(
+ activity = this,
+ playerView = theoplayerView,
+ configuration = config,
+ contentMetadata = metadata,
+ adProcessor = null)
+ }
+
private fun setupYospace() {
yospaceConnector = YospaceConnector(theoplayerView)
}
diff --git a/connectors/analytics/adscript/.gitignore b/connectors/analytics/adscript/.gitignore
new file mode 100644
index 00000000..c23e5a2d
--- /dev/null
+++ b/connectors/analytics/adscript/.gitignore
@@ -0,0 +1,2 @@
+/build
+/libs
\ No newline at end of file
diff --git a/connectors/analytics/adscript/README.md b/connectors/analytics/adscript/README.md
new file mode 100644
index 00000000..4f2daa34
--- /dev/null
+++ b/connectors/analytics/adscript/README.md
@@ -0,0 +1,46 @@
+# Adscript Connector
+
+The Adscript connector provides a [Nielsen Adscript](https://adscript.admosphere.cz/) integration for
+the THEOplayer Android SDK.
+
+## Prerequisites
+
+The Adscript connector requires downloading the private
+[AdScript client library](https://adscript.admosphere.cz/download/AdScriptApiClient_v1.0.10.aar.gz)
+into the app's `libs/` folder. Decompress it and pass the SDK location to the connector
+by setting the `adscriptSdkDir` in your app's `gradle.properties` file:
+
+```bash
+# Location of the adscript SDK
+adscriptSdkDir=./app/libs/
+```
+
+## Usage
+
+Create config and metadata objects, and pass them when building the `AdscriptConnector` instance.
+
+```kotlin
+val config = AdscriptConfiguration(implementationId = "exampleadscript", debug = true)
+val metadata = AdScriptDataObject().apply {
+ set(AdScriptDataObject.FIELD_assetId, "bbb-example")
+ set(AdScriptDataObject.FIELD_type, AdScriptDataObject.OBJ_TYPE_content)
+ set(AdScriptDataObject.FIELD_program, "animation")
+ set(AdScriptDataObject.FIELD_title, "Big Buck Bunny")
+ set(AdScriptDataObject.FIELD_crossId, "1234")
+ set(AdScriptDataObject.FIELD_length, "596000")
+ set(AdScriptDataObject.FIELD_livestream, "0")
+ set(AdScriptDataObject.FIELD_attribute, AdScriptDataObject.ATTRIBUTE_RegularProgram)
+}
+adscriptConnector = AdscriptConnector(
+ activity = this,
+ playerView = theoplayerView,
+ configuration = config,
+ contentMetadata = metadata,
+ adProcessor = null)
+```
+
+The session needs to be started every time the app resumes, so in `onResume` add:
+
+```kotlin
+adscriptConnector.sessionStart()
+```
\ No newline at end of file
diff --git a/connectors/analytics/adscript/build.gradle b/connectors/analytics/adscript/build.gradle
new file mode 100644
index 00000000..bbf07577
--- /dev/null
+++ b/connectors/analytics/adscript/build.gradle
@@ -0,0 +1,71 @@
+import java.util.zip.GZIPInputStream
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace 'com.theoplayer.android.connector.analytics.adscript'
+ compileSdk 35
+
+ defaultConfig {
+ minSdk 21
+ targetSdk 35
+ versionCode 1
+ versionName "1.0"
+
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = '11'
+ }
+}
+
+// The adscript SDK location should be set by the app and passed in a adscriptSdkDir property.
+def aarFileName = 'AdScriptApiClient_v1.0.10.aar'
+def adscriptSdkDir = rootProject.properties['adscriptSdkDir']
+if (!adscriptSdkDir) {
+ logger.warn("⚠️ WARNING: adscriptSdkDir not set.")
+} else if (!rootProject.file(adscriptSdkDir).exists()) {
+ logger.warn("⚠️ WARNING: adscriptSdkDir does not exist at: ${adscriptSdkDir}")
+} else {
+ def aarFile = new File(adscriptSdkDir, aarFileName)
+
+ // The Adscript aar file is typically provided as .gz, check if we still need to extract.
+ def gzFile = new File("${aarFile}.gz")
+ if (!aarFile.exists() && gzFile.exists()) {
+ new FileInputStream(gzFile).withCloseable { fis ->
+ new GZIPInputStream(fis).withCloseable { gis ->
+ new FileOutputStream(aarFile).withCloseable { fos ->
+ fos << gis
+ }
+ }
+ }
+ }
+ dependencies {
+ compileOnly fileTree(dir: "${rootProject.file(adscriptSdkDir)}/$aarFileName", include: ['*.aar', '*.jar'], exclude: [])
+ }
+}
+
+dependencies {
+ compileOnly "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion"
+ compileOnly "com.theoplayer.theoplayer-sdk-android:integration-ads-ima:$sdkVersion"
+ implementation "com.google.android.gms:play-services-ads-identifier:17.0.1"
+ implementation libs.androidx.core.ktx
+ implementation libs.androidx.appcompat
+ implementation libs.androidx.lifecycle.process
+ testImplementation libs.junit
+ androidTestImplementation libs.androidx.test.ext.junit
+ androidTestImplementation libs.androidx.test.espresso.core
+}
\ No newline at end of file
diff --git a/connectors/analytics/adscript/proguard-rules.pro b/connectors/analytics/adscript/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/connectors/analytics/adscript/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/connectors/analytics/adscript/src/main/AndroidManifest.xml b/connectors/analytics/adscript/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..74b7379f
--- /dev/null
+++ b/connectors/analytics/adscript/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptAdapter.kt b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptAdapter.kt
new file mode 100644
index 00000000..18071a7e
--- /dev/null
+++ b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptAdapter.kt
@@ -0,0 +1,496 @@
+package com.theoplayer.android.connector.analytics.adscript
+
+import android.app.Activity
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.ProcessLifecycleOwner
+import com.nad.adscriptapiclient.AdScriptCollector
+import com.nad.adscriptapiclient.AdScriptDataObject
+import com.nad.adscriptapiclient.AdScriptEventEnum
+import com.nad.adscriptapiclient.AdScriptI12n
+import com.nad.adscriptapiclient.AdScriptPlayerState
+import com.nad.adscriptapiclient.AdScriptRunnable
+import com.theoplayer.android.api.THEOplayerView
+import com.theoplayer.android.api.ads.Ad
+import com.theoplayer.android.api.ads.LinearAd
+import com.theoplayer.android.api.event.EventListener
+import com.theoplayer.android.api.event.ads.AdBeginEvent
+import com.theoplayer.android.api.event.ads.AdBreakBeginEvent
+import com.theoplayer.android.api.event.ads.AdBreakEndEvent
+import com.theoplayer.android.api.event.ads.AdEndEvent
+import com.theoplayer.android.api.event.ads.AdFirstQuartileEvent
+import com.theoplayer.android.api.event.ads.AdIntegrationKind
+import com.theoplayer.android.api.event.ads.AdMidpointEvent
+import com.theoplayer.android.api.event.ads.AdThirdQuartileEvent
+import com.theoplayer.android.api.event.ads.AdsEventTypes
+import com.theoplayer.android.api.event.player.DurationChangeEvent
+import com.theoplayer.android.api.event.player.EndedEvent
+import com.theoplayer.android.api.event.player.ErrorEvent
+import com.theoplayer.android.api.event.player.PlayEvent
+import com.theoplayer.android.api.event.player.PlayerEventTypes
+import com.theoplayer.android.api.event.player.PlayingEvent
+import com.theoplayer.android.api.event.player.PresentationModeChange
+import com.theoplayer.android.api.event.player.RateChangeEvent
+import com.theoplayer.android.api.event.player.SourceChangeEvent
+import com.theoplayer.android.api.event.player.TimeUpdateEvent
+import com.theoplayer.android.api.event.player.VolumeChangeEvent
+import com.theoplayer.android.api.player.PresentationMode
+
+private const val TAG = "AdscriptConnector"
+
+data class LogPoint(
+ val name: AdScriptEventEnum, val cue: Double
+)
+
+class AdscriptAdapter(
+ activity: Activity,
+ private val configuration: AdscriptConfiguration,
+ private val playerView: THEOplayerView,
+ private var contentMetadata: AdScriptDataObject?,
+ private val adProcessor: AdProcessor?
+) {
+ private var adMetadata: AdScriptDataObject? = null
+ private var waitingforFirstPlayingOfContent = true
+ private var waitingForFirstSecondOfAd = false
+ private var waitingForFirstSecondOfSsaiAdSince: Double? = null
+ private var contentLogPoints = ArrayDeque()
+ private val adScriptCollector = AdScriptCollector(configuration.implementationId)
+ private val onPlay: EventListener
+ private val onFirstPlaying: EventListener
+ private val onError: EventListener
+ private val onSourceChange: EventListener
+ private val onEnded: EventListener
+ private val onDurationChange: EventListener
+ private val onTimeUpdate: EventListener
+ private val onVolumeChange: EventListener
+ private val onRateChange: EventListener
+ private val onPresentationModeChange: EventListener
+ private val onAdBreakStarted: EventListener
+ private val onAdStarted: EventListener
+ private val onAdFirstQuartile: EventListener
+ private val onAdMidpoint: EventListener
+ private val onAdThirdQuartile: EventListener
+ private val onAdCompleted: EventListener
+ private val onAdBreakEnded: EventListener
+ private val mainHandler = Handler(Looper.getMainLooper())
+ private lateinit var lifecycleObserver: LifecycleObserver
+ private var appInBackground: Boolean = false
+
+ init {
+ Thread(AdScriptRunnable(adScriptCollector, activity)).start()
+
+ onPlay = EventListener { event -> handlePlay(event) }
+ onFirstPlaying = EventListener { event -> handleFirstPlaying(event) }
+ onError = EventListener { event -> handleError(event) }
+ onSourceChange = EventListener { event -> handleSourceChange(event) }
+ onEnded = EventListener { event -> handleEnded(event) }
+ onDurationChange = EventListener { event -> handleDurationChange(event) }
+ onTimeUpdate = EventListener { event -> handleTimeUpdate(event) }
+ onVolumeChange = EventListener { event -> handleVolumeChange(event) }
+ onRateChange = EventListener { event -> handleRateChange(event) }
+ onPresentationModeChange = EventListener { event -> handlePresentationModeChange(event) }
+ onAdBreakStarted = EventListener { event -> handleAdBreakStarted(event) }
+ onAdStarted = EventListener { event -> handleAdStarted(event) }
+ onAdFirstQuartile = EventListener { event -> handleAdFirstQuartile(event) }
+ onAdMidpoint = EventListener { event -> handleAdMidpoint(event) }
+ onAdThirdQuartile = EventListener { event -> handleAdThirdQuartile(event) }
+ onAdCompleted = EventListener { event -> handleAdCompleted(event) }
+ onAdBreakEnded = EventListener { event -> handleAdBreakEnded(event) }
+
+ adScriptCollector.playerState = AdScriptPlayerState()
+ reportPlayerState()
+ addEventListeners()
+ }
+
+ fun start() {
+ adScriptCollector.sessionStart()
+ }
+
+ fun update(metadata: AdScriptDataObject) {
+ contentMetadata = metadata
+ }
+
+ fun updateUser(i12n: AdScriptI12n) {
+ adScriptCollector.i12n = i12n
+ }
+
+ private fun reportPlayerState() {
+ reportFullscreen(playerView.fullScreenManager.isFullScreen)
+ reportDimensions(playerView.player.videoWidth, playerView.player.videoHeight)
+ reportPlaybackSpeed(playerView.player.playbackRate)
+ reportVolumeAndMuted(playerView.player.isMuted, playerView.player.volume)
+ reportTriggeredByUser(playerView.player.isAutoplay)
+ reportVisibility()
+ }
+
+ private fun reportFullscreen(isFullscreen: Boolean) {
+ if (isFullscreen) {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_fullscreen, 1)
+ } else {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_fullscreen, 0)
+ }
+ }
+
+ private fun reportPlaybackSpeed(playbackRate: Double) {
+ if (playbackRate == 1.0) {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_normalSpeed, 1)
+ } else {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_normalSpeed, 0)
+ }
+ }
+
+ private fun reportDimensions(width: Int, height: Int) {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_height, width)
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_width, height)
+ }
+
+ private fun reportVolumeAndMuted(isMuted: Boolean, volume: Double) {
+ if (isMuted || volume == 0.0) {
+ if (configuration.debug) {
+ Log.i(TAG, "Reporting muted (1) & volume (0)")
+ }
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_muted, 1)
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_volume, 0)
+ } else {
+ val volumePercentage = (volume * 100).toInt()
+ if (configuration.debug) {
+ Log.i(TAG, "Reporting muted (0) & volume ($volumePercentage)")
+ }
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_muted, 0)
+ this.adScriptCollector.playerState.set(
+ AdScriptPlayerState.FIELD_volume,
+ volumePercentage
+ )
+ }
+ }
+
+ private fun reportTriggeredByUser(isAutoplay: Boolean) {
+ if (isAutoplay) {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_triggeredByUser, 1)
+ } else {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_triggeredByUser, 0)
+ }
+ }
+
+ private fun reportVisibility() {
+ this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_visibility, 100)
+ }
+
+ private fun reportLogPoint(name: AdScriptEventEnum) {
+ this.adScriptCollector.push(name, contentMetadata)
+ }
+
+ private fun addLogPoints(duration: Double) {
+ if (duration.isFinite()) {
+ contentLogPoints.addLast(LogPoint(AdScriptEventEnum.PROGRESS1, 1.0))
+ contentLogPoints.addLast(LogPoint(AdScriptEventEnum.FIRSTQUARTILE, duration * 0.25))
+ contentLogPoints.addLast(LogPoint(AdScriptEventEnum.MIDPOINT, duration * 0.5))
+ contentLogPoints.addLast(LogPoint(AdScriptEventEnum.THIRDQUARTILE, duration * 0.75))
+ } else {
+ contentLogPoints.addLast(LogPoint(AdScriptEventEnum.PROGRESS1, 1.0))
+ }
+ }
+
+ private fun maybeReportProgress(currentTime: Double) {
+ if (playerView.player.ads.isPlaying) {
+ maybeReportAdProgress(currentTime)
+ return
+ }
+ val nextLogPoint = contentLogPoints.firstOrNull()
+ if (nextLogPoint != null && currentTime >= nextLogPoint.cue) {
+ reportLogPoint(nextLogPoint.name)
+ contentLogPoints.removeFirst()
+ }
+ }
+
+ private fun maybeReportAdProgress(currentTime: Double) {
+ if (!waitingForFirstSecondOfAd) return
+ val currentAd = playerView.player.ads.currentAds.firstOrNull()
+ when (currentAd?.integration) {
+ AdIntegrationKind.GOOGLE_IMA -> {
+ if (currentTime >= 1) {
+ adScriptCollector.push(AdScriptEventEnum.PROGRESS1, adMetadata)
+ waitingForFirstSecondOfAd = false
+ }
+ }
+
+ AdIntegrationKind.GOOGLE_DAI -> {
+ val waitingSince = waitingForFirstSecondOfSsaiAdSince
+ if (waitingSince != null && currentTime >= waitingSince + 1.0) {
+ adScriptCollector.push(AdScriptEventEnum.PROGRESS1, adMetadata)
+ waitingForFirstSecondOfAd = false
+ waitingForFirstSecondOfSsaiAdSince = null
+ }
+ }
+
+ AdIntegrationKind.THEO_ADS -> TODO()
+ AdIntegrationKind.MEDIATAILOR -> TODO()
+ AdIntegrationKind.CUSTOM -> TODO()
+ null -> TODO()
+ }
+ }
+
+ private fun getAdType(timeOffset: Int?): String {
+ return when (timeOffset) {
+ 0 -> {
+ AdScriptDataObject.OBJ_TYPE_preroll
+ }
+
+ -1, playerView.player.duration.toInt() -> {
+ AdScriptDataObject.OBJ_TYPE_postroll
+ }
+
+ else -> {
+ AdScriptDataObject.OBJ_TYPE_midroll
+ }
+ }
+ }
+
+ private fun buildAdMetadata(ad: Ad?) {
+ if (adProcessor != null && ad != null) {
+ adMetadata = adProcessor.apply(ad)
+ } else {
+ val currentAdMetadata = AdScriptDataObject()
+ currentAdMetadata.set(AdScriptDataObject.FIELD_assetId, ad?.id)
+ currentAdMetadata.set(AdScriptDataObject.FIELD_type, getAdType(ad?.adBreak?.timeOffset))
+ if (ad is LinearAd) {
+ currentAdMetadata.set(AdScriptDataObject.FIELD_length, ad.duration)
+ }
+ currentAdMetadata.set(AdScriptDataObject.FIELD_title, ad?.id)
+// currentAdMetadata.set(AdScriptDataObject.FIELD_asmea, "TODO")
+ currentAdMetadata.set(
+ AdScriptDataObject.FIELD_attribute,
+ AdScriptDataObject.ATTRIBUTE_Commercial
+ )
+ adMetadata = currentAdMetadata
+ }
+ }
+
+ private fun addEventListeners() {
+ playerView.player.addEventListener(PlayerEventTypes.PLAY, onPlay)
+ playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying)
+ playerView.player.addEventListener(PlayerEventTypes.ERROR, onError)
+ playerView.player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange)
+ playerView.player.addEventListener(PlayerEventTypes.ENDED, onEnded)
+ playerView.player.addEventListener(PlayerEventTypes.DURATIONCHANGE, onDurationChange)
+ playerView.player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate)
+ playerView.player.addEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange)
+ playerView.player.addEventListener(PlayerEventTypes.RATECHANGE, onRateChange)
+ playerView.player.addEventListener(
+ PlayerEventTypes.PRESENTATIONMODECHANGE,
+ onPresentationModeChange
+ )
+ playerView.player.ads.addEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakStarted)
+ playerView.player.ads.addEventListener(AdsEventTypes.AD_BEGIN, onAdStarted)
+ playerView.player.ads.addEventListener(AdsEventTypes.AD_FIRST_QUARTILE, onAdFirstQuartile)
+ playerView.player.ads.addEventListener(AdsEventTypes.AD_MIDPOINT, onAdMidpoint)
+ playerView.player.ads.addEventListener(AdsEventTypes.AD_THIRD_QUARTILE, onAdThirdQuartile)
+ playerView.player.ads.addEventListener(AdsEventTypes.AD_END, onAdCompleted)
+
+ // Observe app switches between background and foreground
+ lifecycleObserver = LifecycleEventObserver { _, event ->
+ when (event) {
+ // In onResume, you need to call the sessionStart method every time.
+ // {@link https://adscript.admosphere.cz/cz_adScript_Android.html}
+ Lifecycle.Event.ON_RESUME -> {
+ if (appInBackground) {
+ if (configuration.debug) {
+ Log.d(TAG, "onResume")
+ }
+ adScriptCollector.sessionStart()
+ }
+ appInBackground = false
+ }
+
+ Lifecycle.Event.ON_PAUSE -> {
+ if (configuration.debug) {
+ Log.d(TAG, "onPause")
+ }
+ appInBackground = true
+ }
+
+ Lifecycle.Event.ON_DESTROY -> destroy()
+ else -> {/*ignore*/
+ }
+ }
+ }
+ mainHandler.post {
+ ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
+ }
+ }
+
+ fun destroy() {
+ removeEventListeners()
+ }
+
+ private fun removeEventListeners() {
+ playerView.player.removeEventListener(PlayerEventTypes.PLAY, onPlay)
+ playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying)
+ playerView.player.removeEventListener(PlayerEventTypes.ERROR, onError)
+ playerView.player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange)
+ playerView.player.removeEventListener(PlayerEventTypes.ENDED, onEnded)
+ playerView.player.removeEventListener(PlayerEventTypes.DURATIONCHANGE, onDurationChange)
+ playerView.player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate)
+ playerView.player.removeEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange)
+ playerView.player.removeEventListener(PlayerEventTypes.RATECHANGE, onRateChange)
+ playerView.player.removeEventListener(
+ PlayerEventTypes.PRESENTATIONMODECHANGE,
+ onPresentationModeChange
+ )
+ playerView.player.ads.removeEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakStarted)
+ playerView.player.ads.removeEventListener(AdsEventTypes.AD_BEGIN, onAdStarted)
+ playerView.player.ads.removeEventListener(
+ AdsEventTypes.AD_FIRST_QUARTILE,
+ onAdFirstQuartile
+ )
+ playerView.player.ads.removeEventListener(AdsEventTypes.AD_MIDPOINT, onAdMidpoint)
+ playerView.player.ads.removeEventListener(
+ AdsEventTypes.AD_THIRD_QUARTILE,
+ onAdThirdQuartile
+ )
+ playerView.player.ads.removeEventListener(AdsEventTypes.AD_END, onAdCompleted)
+
+ mainHandler.post {
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
+ }
+ }
+
+ private fun handlePlay(event: PlayEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : currentTime = ${event.currentTime}")
+ }
+ }
+
+ private fun handleFirstPlaying(event: PlayingEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : currentTime = ${event.currentTime}")
+ }
+ if (playerView.player.ads.isPlaying) {
+ adScriptCollector.push(AdScriptEventEnum.START, adMetadata)
+ } else if (waitingforFirstPlayingOfContent) {
+ adScriptCollector.push(AdScriptEventEnum.START, contentMetadata)
+ waitingforFirstPlayingOfContent = false // workaround for double playing event
+ }
+ playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying)
+ }
+
+ private fun handleError(event: ErrorEvent) {
+ if (configuration.debug) {
+ Log.d(
+ TAG,
+ "Player Event: ${event.type} : code = ${event.errorObject.code} ; message = ${event.errorObject.message}"
+ )
+ }
+ }
+
+ private fun handleSourceChange(event: SourceChangeEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : source = ${event.source.toString()}")
+ }
+ waitingforFirstPlayingOfContent = true
+ playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying)
+ }
+
+ private fun handleEnded(event: EndedEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : currentTime = ${event.currentTime}")
+ }
+ adScriptCollector.push(AdScriptEventEnum.COMPLETE, contentMetadata)
+ }
+
+ private fun handleDurationChange(event: DurationChangeEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : duration = ${event.duration}")
+ }
+ addLogPoints(event.duration)
+ }
+
+ private fun handleTimeUpdate(event: TimeUpdateEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : currentTime = ${event.currentTime}")
+ }
+ maybeReportProgress(event.currentTime)
+ }
+
+ private fun handleVolumeChange(event: VolumeChangeEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : currentTime = ${event.currentTime}")
+ }
+ reportVolumeAndMuted(playerView.player.isMuted, event.volume)
+ }
+
+ private fun handleRateChange(event: RateChangeEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : currentTime = ${event.currentTime}")
+ }
+ reportPlaybackSpeed(event.playbackRate)
+ }
+
+ private fun handlePresentationModeChange(event: PresentationModeChange) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : presentationMode = ${event.presentationMode}")
+ }
+ reportFullscreen(event.presentationMode === PresentationMode.FULLSCREEN)
+ }
+
+ private fun handleAdBreakStarted(event: AdBreakBeginEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : offset = ${event.adBreak.timeOffset}")
+ }
+ }
+
+ private fun handleAdStarted(event: AdBeginEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : id = ${event.ad?.id}")
+ }
+ buildAdMetadata(event.ad)
+ waitingForFirstSecondOfAd = true
+ if (event.ad?.integration == AdIntegrationKind.GOOGLE_DAI) {
+ waitingForFirstSecondOfSsaiAdSince = playerView.player.currentTime
+ }
+ adScriptCollector.push(AdScriptEventEnum.START, adMetadata)
+ }
+
+ private fun handleAdFirstQuartile(event: AdFirstQuartileEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : id = ${event.ad?.id}")
+ }
+ adScriptCollector.push(AdScriptEventEnum.FIRSTQUARTILE, adMetadata)
+
+ }
+
+ private fun handleAdMidpoint(event: AdMidpointEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : id = ${event.ad?.id}")
+ }
+ adScriptCollector.push(AdScriptEventEnum.MIDPOINT, adMetadata)
+ }
+
+ private fun handleAdThirdQuartile(event: AdThirdQuartileEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : id = ${event.ad?.id}")
+ }
+ adScriptCollector.push(AdScriptEventEnum.THIRDQUARTILE, adMetadata)
+ }
+
+ private fun handleAdCompleted(event: AdEndEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : id = ${event.ad?.id}")
+ }
+ adScriptCollector.push(AdScriptEventEnum.COMPLETE, adMetadata)
+
+ }
+
+ private fun handleAdBreakEnded(event: AdBreakEndEvent) {
+ if (configuration.debug) {
+ Log.d(TAG, "Player Event: ${event.type} : timeOffset = ${event.adBreak.timeOffset}")
+ }
+ if (event.adBreak.timeOffset == 0) {
+ playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying)
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConfiguration.kt b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConfiguration.kt
new file mode 100644
index 00000000..d19a3a86
--- /dev/null
+++ b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConfiguration.kt
@@ -0,0 +1,6 @@
+package com.theoplayer.android.connector.analytics.adscript
+
+data class AdscriptConfiguration(
+ val implementationId: String,
+ val debug: Boolean
+)
\ No newline at end of file
diff --git a/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConnector.kt b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConnector.kt
new file mode 100644
index 00000000..f292d5c2
--- /dev/null
+++ b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConnector.kt
@@ -0,0 +1,42 @@
+package com.theoplayer.android.connector.analytics.adscript
+
+import android.app.Activity
+import com.nad.adscriptapiclient.AdScriptDataObject
+import com.nad.adscriptapiclient.AdScriptI12n
+import com.theoplayer.android.api.THEOplayerView
+import com.theoplayer.android.api.ads.Ad
+
+interface AdProcessor {
+ fun apply(input: Ad): AdScriptDataObject
+}
+
+class AdscriptConnector(
+ activity: Activity,
+ playerView: THEOplayerView,
+ configuration: AdscriptConfiguration,
+ contentMetadata: AdScriptDataObject,
+ adProcessor: AdProcessor?
+) {
+ private val adscriptAdapter =
+ AdscriptAdapter(activity, configuration, playerView, contentMetadata, adProcessor)
+
+ init {
+ adscriptAdapter.start()
+ }
+
+ fun updateMetadata(metadata: AdScriptDataObject) {
+ adscriptAdapter.update(metadata)
+ }
+
+ fun sessionStart() {
+ adscriptAdapter.start()
+ }
+
+ fun updateUser(i12n: AdScriptI12n) {
+ adscriptAdapter.updateUser(i12n)
+ }
+
+ fun destroy() {
+ adscriptAdapter.destroy()
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 22f41cdc..127684b6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -26,3 +26,6 @@ android.nonFinalResIds=true
groupId=com.theoplayer.android-connector
sdkVersion=9.8.2
connectorVersion=9.8.2
+
+# Location of the adscript SDK
+adscriptSdkDir=./app/libs/
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index dcdf50c3..0fd6feb7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -28,6 +28,7 @@ include ':app'
include ':connectors:analytics:comscore'
include ':connectors:analytics:conviva'
include ':connectors:analytics:nielsen'
+include ':connectors:analytics:adscript'
include ':connectors:mediasession'
include ':connectors:yospace'
include ':connectors:uplynk'