From 5af00f34ea26b1159a58fa780ca24057b37b909c Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:35:01 +0200 Subject: [PATCH 01/18] initial implementation --- .../analytics/adscript/AdscriptAdapter.kt | 428 ++++++++++++++++++ .../adscript/AdscriptConfiguration.kt | 6 + .../analytics/adscript/AdscriptConnector.kt | 37 ++ 3 files changed, 471 insertions(+) create mode 100644 connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptAdapter.kt create mode 100644 connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConfiguration.kt create mode 100644 connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConnector.kt 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..591d848a --- /dev/null +++ b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptAdapter.kt @@ -0,0 +1,428 @@ +package com.theoplayer.android.connector.analytics.adscript + +import android.app.Activity +import android.util.Log +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 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 + + 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) + } + +// 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) +// } + +// window.addEventListener('resize', this.reportPlayerState) +// window.addEventListener('blur', this.reportPlayerState) +// window.addEventListener('focus', this.reportPlayerState) +// document.addEventListener('scroll', this.reportPlayerState) +// document.addEventListener('visibilitychange', this.reportPlayerState) + + + 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 { + adScriptCollector.push(AdScriptEventEnum.START, contentMetadata) + } + 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()}") + } + 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..1711f551 --- /dev/null +++ b/connectors/analytics/adscript/src/main/java/com/theoplayer/android/connector/analytics/adscript/AdscriptConnector.kt @@ -0,0 +1,37 @@ +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) + } +} \ No newline at end of file From c03e3e8aad62b9140f62c44122916dd20bcb584e Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:35:47 +0200 Subject: [PATCH 02/18] readme placeholder --- connectors/analytics/adscript/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 connectors/analytics/adscript/README.md diff --git a/connectors/analytics/adscript/README.md b/connectors/analytics/adscript/README.md new file mode 100644 index 00000000..1a7df487 --- /dev/null +++ b/connectors/analytics/adscript/README.md @@ -0,0 +1,3 @@ +# Adscript Connector + +To do \ No newline at end of file From 0ca39754b96ff6d04106fbc52f01adf1086c3917 Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:36:34 +0200 Subject: [PATCH 03/18] bp --- connectors/analytics/adscript/.gitignore | 2 + connectors/analytics/adscript/build.gradle | 43 +++++++++++++++++++ .../analytics/adscript/proguard-rules.pro | 21 +++++++++ .../adscript/src/main/AndroidManifest.xml | 3 ++ 4 files changed, 69 insertions(+) create mode 100644 connectors/analytics/adscript/.gitignore create mode 100644 connectors/analytics/adscript/build.gradle create mode 100644 connectors/analytics/adscript/proguard-rules.pro create mode 100644 connectors/analytics/adscript/src/main/AndroidManifest.xml 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/build.gradle b/connectors/analytics/adscript/build.gradle new file mode 100644 index 00000000..ac432c78 --- /dev/null +++ b/connectors/analytics/adscript/build.gradle @@ -0,0 +1,43 @@ +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' + } +} + +dependencies { + compileOnly fileTree(dir: './libs/AdScriptApiClient_v1.0.10.aar', include: ['*.aar', '*.jar'], exclude: []) + 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 + 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 From d674eb04c2108ea581155bea17a22b1a75570ba1 Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:36:50 +0200 Subject: [PATCH 04/18] include adscript --- settings.gradle | 1 + 1 file changed, 1 insertion(+) 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' From 709168ae7f41e5bb4ab859079587aaea0acf4526 Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:37:25 +0200 Subject: [PATCH 05/18] include adscript --- app/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) 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') + } From 5d77ecaf13b95def203f3bca7b62732c4044e557 Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:39:48 +0200 Subject: [PATCH 06/18] ignore vendor aar files --- app/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/.gitignore b/app/.gitignore index 42afabfd..c23e5a2d 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ -/build \ No newline at end of file +/build +/libs \ No newline at end of file From 1115382a0d4091d19319d45a074dfc82a42c8b23 Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:42:27 +0200 Subject: [PATCH 07/18] add adscript connector to example --- .../android/connector/MainActivity.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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..9ee908a1 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,23 @@ class MainActivity : ComponentActivity() { nielsenConnector = NielsenConnector(applicationContext, theoplayerView.player, appId, true) } + private fun setupAdscript() { + val implementationId = "exampleadscript" + + val config = AdscriptConfiguration(implementationId,true) + val metadata = AdScriptDataObject() + metadata.set(AdScriptDataObject.FIELD_assetId, "bbb-example"); + metadata.set(AdScriptDataObject.FIELD_type, AdScriptDataObject.OBJ_TYPE_content); + metadata.set(AdScriptDataObject.FIELD_program, "animation"); + metadata.set(AdScriptDataObject.FIELD_title, "Big Buck Bunny"); + metadata.set(AdScriptDataObject.FIELD_crossId, "1234"); + metadata.set(AdScriptDataObject.FIELD_length, "596000"); + metadata.set(AdScriptDataObject.FIELD_livestream, "0"); + metadata.set(AdScriptDataObject.FIELD_attribute, AdScriptDataObject.ATTRIBUTE_RegularProgram); + + var adscriptConnector = AdscriptConnector(this, theoplayerView, config, metadata, null) + } + private fun setupYospace() { yospaceConnector = YospaceConnector(theoplayerView) } From c075dba3c7c0d4a046fa0e31f14d4765dc570d37 Mon Sep 17 00:00:00 2001 From: Wonne Joosen Date: Fri, 13 Jun 2025 23:59:53 +0200 Subject: [PATCH 08/18] work around double playing event --- .../android/connector/analytics/adscript/AdscriptAdapter.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index 591d848a..4f73b50e 100644 --- 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 @@ -48,6 +48,7 @@ class AdscriptAdapter( 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() @@ -308,8 +309,9 @@ class AdscriptAdapter( } if (playerView.player.ads.isPlaying) { adScriptCollector.push(AdScriptEventEnum.START, adMetadata) - } else { + } else if (waitingforFirstPlayingOfContent){ adScriptCollector.push(AdScriptEventEnum.START, contentMetadata) + waitingforFirstPlayingOfContent = false // workaround for double playing event } playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) } @@ -324,6 +326,7 @@ class AdscriptAdapter( if (configuration.debug) { Log.d(TAG, "Player Event: ${event.type} : source = ${event.source.toString()}") } + waitingforFirstPlayingOfContent = true playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) } From 962114f40e2747b89b495880cf1d4de463a524a3 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 29 Jul 2025 14:30:06 +0200 Subject: [PATCH 09/18] Use adscriptSdkDir property to set SDK location --- app/.gitignore | 1 - app/libs/.gitignore | 1 + connectors/analytics/adscript/build.gradle | 10 +++++++++- gradle.properties | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 app/libs/.gitignore diff --git a/app/.gitignore b/app/.gitignore index c23e5a2d..796b96d1 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1 @@ /build -/libs \ No newline at end of file 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/connectors/analytics/adscript/build.gradle b/connectors/analytics/adscript/build.gradle index ac432c78..088df19e 100644 --- a/connectors/analytics/adscript/build.gradle +++ b/connectors/analytics/adscript/build.gradle @@ -30,8 +30,16 @@ android { } } +// The adscript SDK location should be set by the app and passed in a adscriptSdkDir property. +def adscriptSdkDir = rootProject.properties['adscriptSdkDir'] +if (!adscriptSdkDir) { + logger.warn("⚠️ WARNING: adscriptSdkDir not set.") +} else if (!rootProject.file("ded").exists()) { + logger.warn("⚠️ WARNING: adscriptSdkDir does not exist at: ${adscriptSdkDir}") +} + dependencies { - compileOnly fileTree(dir: './libs/AdScriptApiClient_v1.0.10.aar', include: ['*.aar', '*.jar'], exclude: []) + compileOnly fileTree(dir: "$adscriptSdkDir/AdScriptApiClient_v1.0.10.aar", include: ['*.aar', '*.jar'], exclude: []) 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" 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 From 5dbb4ae27753d3b432b5ba0b04f31d090b015918 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 29 Jul 2025 14:48:28 +0200 Subject: [PATCH 10/18] Optimize SDK dependency --- connectors/analytics/adscript/build.gradle | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/connectors/analytics/adscript/build.gradle b/connectors/analytics/adscript/build.gradle index 088df19e..8e2048c6 100644 --- a/connectors/analytics/adscript/build.gradle +++ b/connectors/analytics/adscript/build.gradle @@ -34,12 +34,15 @@ android { def adscriptSdkDir = rootProject.properties['adscriptSdkDir'] if (!adscriptSdkDir) { logger.warn("⚠️ WARNING: adscriptSdkDir not set.") -} else if (!rootProject.file("ded").exists()) { - logger.warn("⚠️ WARNING: adscriptSdkDir does not exist at: ${adscriptSdkDir}") +} else if (!rootProject.file(adscriptSdkDir).exists()) { + logger.warn("⚠️ WARNING: adscriptSdkDir does not exist at: ${adscriptSdkDir}") +} else { + dependencies { + compileOnly fileTree(dir: "${rootProject.file(adscriptSdkDir)}/AdScriptApiClient_v1.0.10.aar", include: ['*.aar', '*.jar'], exclude: []) + } } dependencies { - compileOnly fileTree(dir: "$adscriptSdkDir/AdScriptApiClient_v1.0.10.aar", include: ['*.aar', '*.jar'], exclude: []) 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" From 6d6ed8b095c47b8df3e467960979f96796c8a327 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 29 Jul 2025 14:48:41 +0200 Subject: [PATCH 11/18] Clean up adscriptConnector usage --- .../android/connector/MainActivity.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) 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 9ee908a1..d4cc5ec2 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -195,20 +195,23 @@ class MainActivity : ComponentActivity() { } private fun setupAdscript() { - val implementationId = "exampleadscript" - - val config = AdscriptConfiguration(implementationId,true) - val metadata = AdScriptDataObject() - metadata.set(AdScriptDataObject.FIELD_assetId, "bbb-example"); - metadata.set(AdScriptDataObject.FIELD_type, AdScriptDataObject.OBJ_TYPE_content); - metadata.set(AdScriptDataObject.FIELD_program, "animation"); - metadata.set(AdScriptDataObject.FIELD_title, "Big Buck Bunny"); - metadata.set(AdScriptDataObject.FIELD_crossId, "1234"); - metadata.set(AdScriptDataObject.FIELD_length, "596000"); - metadata.set(AdScriptDataObject.FIELD_livestream, "0"); - metadata.set(AdScriptDataObject.FIELD_attribute, AdScriptDataObject.ATTRIBUTE_RegularProgram); - - var adscriptConnector = AdscriptConnector(this, theoplayerView, config, metadata, null) + 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() { From 5af01392a81384af61ce46523849bc2acbfa52c0 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 4 Aug 2025 09:32:20 +0200 Subject: [PATCH 12/18] Add readme --- connectors/analytics/adscript/README.md | 45 ++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/connectors/analytics/adscript/README.md b/connectors/analytics/adscript/README.md index 1a7df487..4f2daa34 100644 --- a/connectors/analytics/adscript/README.md +++ b/connectors/analytics/adscript/README.md @@ -1,3 +1,46 @@ # Adscript Connector -To do \ No newline at end of file +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 From ae74e50d15e3398661ec7b4a01cabf4ed48e13b2 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 4 Aug 2025 11:28:07 +0200 Subject: [PATCH 13/18] Allow drop-in of gz aar file --- connectors/analytics/adscript/build.gradle | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/connectors/analytics/adscript/build.gradle b/connectors/analytics/adscript/build.gradle index 8e2048c6..77c6a277 100644 --- a/connectors/analytics/adscript/build.gradle +++ b/connectors/analytics/adscript/build.gradle @@ -1,3 +1,5 @@ +import java.util.zip.GZIPInputStream + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -31,14 +33,28 @@ android { } // 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)}/AdScriptApiClient_v1.0.10.aar", include: ['*.aar', '*.jar'], exclude: []) + compileOnly fileTree(dir: "${rootProject.file(adscriptSdkDir)}/$aarFileName", include: ['*.aar', '*.jar'], exclude: []) } } From f044d903d52f090cb2309f15f56a90e8e31f977e Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 4 Aug 2025 13:13:40 +0200 Subject: [PATCH 14/18] Add destroy --- .../analytics/adscript/AdscriptAdapter.kt | 47 +++++++++---------- .../analytics/adscript/AdscriptConnector.kt | 4 ++ 2 files changed, 26 insertions(+), 25 deletions(-) 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 index 4f73b50e..b6c7a2e8 100644 --- 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 @@ -252,7 +252,7 @@ class AdscriptAdapter( } } - private fun addEventListeners(){ + private fun addEventListeners() { playerView.player.addEventListener(PlayerEventTypes.PLAY, onPlay) playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) playerView.player.addEventListener(PlayerEventTypes.ERROR, onError) @@ -271,31 +271,28 @@ class AdscriptAdapter( playerView.player.ads.addEventListener(AdsEventTypes.AD_END, onAdCompleted) } -// 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) -// } - -// window.addEventListener('resize', this.reportPlayerState) -// window.addEventListener('blur', this.reportPlayerState) -// window.addEventListener('focus', this.reportPlayerState) -// document.addEventListener('scroll', this.reportPlayerState) -// document.addEventListener('visibilitychange', this.reportPlayerState) + 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) + } private fun handlePlay(event: PlayEvent) { if (configuration.debug) { 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 index 1711f551..373a07fa 100644 --- 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 @@ -34,4 +34,8 @@ class AdscriptConnector( fun updateUser(i12n: AdScriptI12n) { adscriptAdapter.updateUser(i12n) } + + fun destroy() { + adscriptAdapter.destroy() + } } \ No newline at end of file From ca84fb599ec66da81b563a09f2fa54298f551fe9 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 8 Aug 2025 14:53:04 +0200 Subject: [PATCH 15/18] Add sessionStart on each resume --- .../analytics/adscript/AdscriptAdapter.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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 index b6c7a2e8..73c10f33 100644 --- 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 @@ -1,7 +1,13 @@ 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 @@ -70,6 +76,8 @@ class AdscriptAdapter( private val onAdThirdQuartile: EventListener private val onAdCompleted: EventListener private val onAdBreakEnded: EventListener + private val mainHandler = Handler(Looper.getMainLooper()) + private lateinit var lifecycleObserver: LifecycleObserver init { Thread(AdScriptRunnable(adScriptCollector, activity)).start() @@ -269,6 +277,23 @@ class AdscriptAdapter( 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 -> { + adScriptCollector.sessionStart() + } + Lifecycle.Event.ON_DESTROY -> destroy() + else -> {/*ignore*/ + } + } + } + mainHandler.post { + ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver) + } } fun destroy() { @@ -292,6 +317,10 @@ class AdscriptAdapter( 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) { From d768933555edda2cf8dbeb320fb5e1f02d506d8e Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 8 Aug 2025 14:53:22 +0200 Subject: [PATCH 16/18] Add dependency --- connectors/analytics/adscript/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/connectors/analytics/adscript/build.gradle b/connectors/analytics/adscript/build.gradle index 77c6a277..bbf07577 100644 --- a/connectors/analytics/adscript/build.gradle +++ b/connectors/analytics/adscript/build.gradle @@ -64,6 +64,7 @@ dependencies { 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 From af3bf58e9660f140dbc25456ce499683f2f7a299 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 8 Aug 2025 15:38:51 +0200 Subject: [PATCH 17/18] Start session when resuming app from background --- .../analytics/adscript/AdscriptAdapter.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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 index 73c10f33..5821a247 100644 --- 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 @@ -78,6 +78,7 @@ class AdscriptAdapter( 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() @@ -105,7 +106,7 @@ class AdscriptAdapter( addEventListeners() } - fun start(){ + fun start() { adScriptCollector.sessionStart() } @@ -284,8 +285,22 @@ class AdscriptAdapter( // In onResume, you need to call the sessionStart method every time. // {@link https://adscript.admosphere.cz/cz_adScript_Android.html} Lifecycle.Event.ON_RESUME -> { - adScriptCollector.sessionStart() + 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*/ } From 8ebda3c424c185b56cb1b24c1c133c3ab6c6a056 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 8 Aug 2025 15:39:35 +0200 Subject: [PATCH 18/18] Reformat --- .../analytics/adscript/AdscriptAdapter.kt | 58 +++++++++++++------ .../analytics/adscript/AdscriptConnector.kt | 3 +- 2 files changed, 43 insertions(+), 18 deletions(-) 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 index 5821a247..18071a7e 100644 --- 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 @@ -43,7 +43,7 @@ import com.theoplayer.android.api.player.PresentationMode private const val TAG = "AdscriptConnector" data class LogPoint( - val name: AdScriptEventEnum, val cue: Double + val name: AdScriptEventEnum, val cue: Double ) class AdscriptAdapter( @@ -118,7 +118,7 @@ class AdscriptAdapter( adScriptCollector.i12n = i12n } - private fun reportPlayerState(){ + private fun reportPlayerState() { reportFullscreen(playerView.fullScreenManager.isFullScreen) reportDimensions(playerView.player.videoWidth, playerView.player.videoHeight) reportPlaybackSpeed(playerView.player.playbackRate) @@ -151,17 +151,20 @@ class AdscriptAdapter( private fun reportVolumeAndMuted(isMuted: Boolean, volume: Double) { if (isMuted || volume == 0.0) { if (configuration.debug) { - Log.i(TAG,"Reporting muted (1) & volume (0)") + 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)") + Log.i(TAG, "Reporting muted (0) & volume ($volumePercentage)") } this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_muted, 0) - this.adScriptCollector.playerState.set(AdScriptPlayerState.FIELD_volume, volumePercentage) + this.adScriptCollector.playerState.set( + AdScriptPlayerState.FIELD_volume, + volumePercentage + ) } } @@ -198,7 +201,7 @@ class AdscriptAdapter( return } val nextLogPoint = contentLogPoints.firstOrNull() - if (nextLogPoint != null && currentTime >= nextLogPoint.cue ) { + if (nextLogPoint != null && currentTime >= nextLogPoint.cue) { reportLogPoint(nextLogPoint.name) contentLogPoints.removeFirst() } @@ -210,14 +213,15 @@ class AdscriptAdapter( when (currentAd?.integration) { AdIntegrationKind.GOOGLE_IMA -> { if (currentTime >= 1) { - adScriptCollector.push(AdScriptEventEnum.PROGRESS1,adMetadata) + 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) + if (waitingSince != null && currentTime >= waitingSince + 1.0) { + adScriptCollector.push(AdScriptEventEnum.PROGRESS1, adMetadata) waitingForFirstSecondOfAd = false waitingForFirstSecondOfSsaiAdSince = null } @@ -235,9 +239,11 @@ class AdscriptAdapter( 0 -> { AdScriptDataObject.OBJ_TYPE_preroll } + -1, playerView.player.duration.toInt() -> { AdScriptDataObject.OBJ_TYPE_postroll } + else -> { AdScriptDataObject.OBJ_TYPE_midroll } @@ -256,7 +262,10 @@ class AdscriptAdapter( } currentAdMetadata.set(AdScriptDataObject.FIELD_title, ad?.id) // currentAdMetadata.set(AdScriptDataObject.FIELD_asmea, "TODO") - currentAdMetadata.set(AdScriptDataObject.FIELD_attribute, AdScriptDataObject.ATTRIBUTE_Commercial) + currentAdMetadata.set( + AdScriptDataObject.FIELD_attribute, + AdScriptDataObject.ATTRIBUTE_Commercial + ) adMetadata = currentAdMetadata } } @@ -271,7 +280,10 @@ class AdscriptAdapter( 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.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) @@ -315,7 +327,7 @@ class AdscriptAdapter( removeEventListeners() } - private fun removeEventListeners(){ + private fun removeEventListeners() { playerView.player.removeEventListener(PlayerEventTypes.PLAY, onPlay) playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) playerView.player.removeEventListener(PlayerEventTypes.ERROR, onError) @@ -325,12 +337,21 @@ class AdscriptAdapter( 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.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_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_THIRD_QUARTILE, + onAdThirdQuartile + ) playerView.player.ads.removeEventListener(AdsEventTypes.AD_END, onAdCompleted) mainHandler.post { @@ -350,7 +371,7 @@ class AdscriptAdapter( } if (playerView.player.ads.isPlaying) { adScriptCollector.push(AdScriptEventEnum.START, adMetadata) - } else if (waitingforFirstPlayingOfContent){ + } else if (waitingforFirstPlayingOfContent) { adScriptCollector.push(AdScriptEventEnum.START, contentMetadata) waitingforFirstPlayingOfContent = false // workaround for double playing event } @@ -359,7 +380,10 @@ class AdscriptAdapter( private fun handleError(event: ErrorEvent) { if (configuration.debug) { - Log.d(TAG, "Player Event: ${event.type} : code = ${event.errorObject.code} ; message = ${event.errorObject.message}") + Log.d( + TAG, + "Player Event: ${event.type} : code = ${event.errorObject.code} ; message = ${event.errorObject.message}" + ) } } 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 index 373a07fa..f292d5c2 100644 --- 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 @@ -17,7 +17,8 @@ class AdscriptConnector( contentMetadata: AdScriptDataObject, adProcessor: AdProcessor? ) { - private val adscriptAdapter = AdscriptAdapter(activity, configuration, playerView, contentMetadata, adProcessor) + private val adscriptAdapter = + AdscriptAdapter(activity, configuration, playerView, contentMetadata, adProcessor) init { adscriptAdapter.start()