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'