From 3cf4ef22f2fa3ec033c2980c114fdcd5a7e78312 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:12:15 +0000 Subject: [PATCH 1/3] Report THEOads interstitial errors as failed ads to Conviva Co-Authored-By: benny.tepfer --- .../analytics/conviva/ads/AdReporter.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt index 420ba137..01a5a64d 100644 --- a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt +++ b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt @@ -10,6 +10,11 @@ import com.theoplayer.android.api.ads.AdBreak import com.theoplayer.android.api.ads.ima.GoogleImaAd import com.theoplayer.android.api.ads.ima.GoogleImaAdEvent import com.theoplayer.android.api.ads.ima.GoogleImaAdEventType +import com.theoplayer.android.api.ads.theoads.InterstitialType +import com.theoplayer.android.api.ads.theoads.TheoAdsIntegration +import com.theoplayer.android.api.ads.theoads.event.InterstitialErrorEvent +import com.theoplayer.android.api.ads.theoads.event.TheoAdsEventTypes +import com.theoplayer.android.api.ads.theoads.theoAds import com.theoplayer.android.api.event.EventDispatcher import com.theoplayer.android.api.event.EventListener import com.theoplayer.android.api.event.ads.AdBeginEvent @@ -63,6 +68,17 @@ class AdReporter( private val onAdBreakEnd: EventListener private val onAdSkip: EventListener private val onAdError: EventListener + private val onInterstitialError: EventListener + + private val theoAds: TheoAdsIntegration? = try { + player.theoAds + } catch (_: IllegalStateException) { + // The THEOads integration was not added to the player. + null + } catch (_: NoClassDefFoundError) { + // The THEOads integration is not part of the application. + null + } init { convivaAdAnalytics.setCallback(this) @@ -156,6 +172,10 @@ class AdReporter( handleAdError() } + onInterstitialError = EventListener { event -> + handleInterstitialError(event) + } + addEventListeners() } @@ -197,6 +217,8 @@ class AdReporter( onImaContentResume ) } + + theoAds?.addEventListener(TheoAdsEventTypes.INTERSTITIAL_ERROR, onInterstitialError) } private fun removeEventListeners() { @@ -221,6 +243,8 @@ class AdReporter( onImaContentResume ) } + + theoAds?.removeEventListener(TheoAdsEventTypes.INTERSTITIAL_ERROR, onInterstitialError) } private fun handleAdBreakBegin(adBreak: AdBreak?, isLinearAdBreak: Boolean) { @@ -350,6 +374,43 @@ class AdReporter( convivaAdAnalytics.reportAdFailed("Ad Request Failed") } + /** + * A THEOads (SGAI) ad break can fail before any ad is available, for example when the ad server + * returns an empty VAST response. In that case no ad break or ad events are dispatched, so report + * the attempted ad break as a failed ad to keep Conviva's ad attempt and fill rate metrics correct. + */ + private fun handleInterstitialError(event: InterstitialErrorEvent) { + val interstitial = event.interstitial + if (interstitial.type != InterstitialType.ADBREAK || currentAdBreak != null) { + return + } + + // Make sure the session is started + convivaHandler.maybeReportPlaybackRequested() + + adBreakCounter++ + convivaVideoAnalytics.reportAdBreakStarted( + ConvivaSdkConstants.AdPlayer.CONTENT, + ConvivaSdkConstants.AdType.SERVER_SIDE, + mapOf( + ConvivaSdkConstants.POD_DURATION to (interstitial.duration ?: 0.0), + ConvivaSdkConstants.POD_INDEX to adBreakCounter + ) + ) + val adMetadata = mapOf( + "c3.csid" to convivaVideoAnalytics.sessionId.toString(), + "contentAssetName" to convivaHandler.contentAssetName, + "c3.ad.technology" to "Server Guided", + ConvivaSdkConstants.IS_LIVE to false, + ) + if (BuildConfig.DEBUG) { + Log.d(TAG, "reportAdFailed - ${event.message}") + } + convivaAdAnalytics.setAdInfo(adMetadata) + convivaAdAnalytics.reportAdFailed(event.message ?: "No ad available") + convivaVideoAnalytics.reportAdBreakEnded() + } + fun reset() { // Optionally report end of current Ad if (currentAd != null) { From 36ebdc06ffe38fa714a0539ea262e80d5e08dd51 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 13 Aug 2026 12:29:44 -0700 Subject: [PATCH 2/3] Refactor interstitial error reporting per review, align with web - Add calculateInterstitialAdBreakInfo() including the missing POD_POSITION derived from the interstitial start time, and fix POD_DURATION to an Int to match calculateCurrentAdBreakInfo(). - Add POD_POSITION to calculateCurrentAdBreakInfo() as well, matching the web connector. - Extract collectBaseAdMetadata() for the c3.csid and contentAssetName tags shared by successful and failed ad reporting, and replace the scattered "Server Guided" literals with the SGAI_AD_TYPE constant. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../analytics/conviva/ads/AdReporter.kt | 34 ++++++++++--------- .../analytics/conviva/utils/Utils.kt | 27 ++++++++++++++- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt index 01a5a64d..5dcc8928 100644 --- a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt +++ b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt @@ -28,9 +28,11 @@ import com.theoplayer.android.api.event.player.* import com.theoplayer.android.api.player.Player import com.theoplayer.android.connector.analytics.conviva.BuildConfig import com.theoplayer.android.connector.analytics.conviva.ConvivaHandlerBase +import com.theoplayer.android.connector.analytics.conviva.utils.SGAI_AD_TYPE import com.theoplayer.android.connector.analytics.conviva.utils.calculateAdType import com.theoplayer.android.connector.analytics.conviva.utils.calculateAdTypeAsString import com.theoplayer.android.connector.analytics.conviva.utils.calculateCurrentAdBreakInfo +import com.theoplayer.android.connector.analytics.conviva.utils.calculateInterstitialAdBreakInfo import com.theoplayer.android.connector.analytics.conviva.utils.collectAdMetadata import com.theoplayer.android.connector.analytics.conviva.utils.collectPlayerInfo import com.theoplayer.android.connector.analytics.conviva.utils.updateAdMetadataForGoogleIma @@ -247,6 +249,18 @@ class AdReporter( theoAds?.removeEventListener(TheoAdsEventTypes.INTERSTITIAL_ERROR, onInterstitialError) } + /** + * Ad metadata shared between successful and failed ad reporting. + * Every session ad or content has its session ID. In order to “attach” an ad to its respective content session, + * there are two tags that are critical: + * - `c3.csid`: the content’s sessionID; + * - `contentAssetName`: the content's assetName. + */ + private fun collectBaseAdMetadata(): Map = mapOf( + "c3.csid" to convivaVideoAnalytics.sessionId.toString(), + "contentAssetName" to convivaHandler.contentAssetName, + ) + private fun handleAdBreakBegin(adBreak: AdBreak?, isLinearAdBreak: Boolean) { // Make sure the session is started convivaHandler.maybeReportPlaybackRequested() @@ -279,15 +293,8 @@ class AdReporter( } if (ad != null && isAdLinear(ad)) { currentAd = ad - // Every session ad or content has its session ID. In order to “attach” an ad to its respective content session, - // there are two tags that are critical: - // - `c3.csid`: the content’s sessionID; - // - `contentAssetName`: the content's assetName. - val contentAssetName = convivaHandler.contentAssetName val adTechnology = calculateAdTypeAsString(ad) - var adMetadata = collectAdMetadata(ad) + mapOf( - "c3.csid" to convivaVideoAnalytics.sessionId.toString(), - "contentAssetName" to contentAssetName, + var adMetadata = collectAdMetadata(ad) + collectBaseAdMetadata() + mapOf( "c3.ad.technology" to adTechnology, ConvivaSdkConstants.IS_LIVE to false, ) @@ -392,15 +399,10 @@ class AdReporter( convivaVideoAnalytics.reportAdBreakStarted( ConvivaSdkConstants.AdPlayer.CONTENT, ConvivaSdkConstants.AdType.SERVER_SIDE, - mapOf( - ConvivaSdkConstants.POD_DURATION to (interstitial.duration ?: 0.0), - ConvivaSdkConstants.POD_INDEX to adBreakCounter - ) + calculateInterstitialAdBreakInfo(interstitial, adBreakCounter) ) - val adMetadata = mapOf( - "c3.csid" to convivaVideoAnalytics.sessionId.toString(), - "contentAssetName" to convivaHandler.contentAssetName, - "c3.ad.technology" to "Server Guided", + val adMetadata = collectBaseAdMetadata() + mapOf( + "c3.ad.technology" to SGAI_AD_TYPE, ConvivaSdkConstants.IS_LIVE to false, ) if (BuildConfig.DEBUG) { diff --git a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/utils/Utils.kt b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/utils/Utils.kt index d3a1e080..360bdb1f 100644 --- a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/utils/Utils.kt +++ b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/utils/Utils.kt @@ -8,6 +8,7 @@ import com.theoplayer.android.api.ads.Ad import com.theoplayer.android.api.ads.AdBreak import com.theoplayer.android.api.ads.LinearAd import com.theoplayer.android.api.ads.ima.GoogleImaAd +import com.theoplayer.android.api.ads.theoads.Interstitial import com.theoplayer.android.api.ads.theoads.TheoAdDescription import com.theoplayer.android.api.event.ads.AdIntegrationKind import com.theoplayer.android.api.player.Player @@ -37,9 +38,15 @@ fun calculateAdType(adBreak: AdBreak): ConvivaSdkConstants.AdType { } } +/** + * The ad technology reported to Conviva for THEOads (SGAI). + * SGAI isn't officially supported by Conviva yet, so we report it with our own string for now. + */ +const val SGAI_AD_TYPE = "Server Guided" + fun calculateAdTypeAsString(ad: Ad): String { if (ad.integration == AdIntegrationKind.THEO_ADS) { - return "Server Guided" + return SGAI_AD_TYPE } return when (calculateAdType(ad)) { ConvivaSdkConstants.AdType.SERVER_SIDE -> "Server Side" @@ -58,11 +65,29 @@ fun calculateCurrentAdBreakPosition(adBreak: AdBreak): String { fun calculateCurrentAdBreakInfo(adBreak: AdBreak, adBreakIndex: Int): Map { return mapOf( + ConvivaSdkConstants.POD_POSITION to calculateCurrentAdBreakPosition(adBreak), ConvivaSdkConstants.POD_DURATION to adBreak.maxDuration, ConvivaSdkConstants.POD_INDEX to adBreakIndex ) } +fun calculateInterstitialAdBreakPosition(interstitial: Interstitial): String { + val startTime = interstitial.startTime + return when { + startTime == 0.0 -> "Pre-roll" + startTime < 0.0 || !startTime.isFinite() -> "Post-roll" + else -> "Mid-roll" + } +} + +fun calculateInterstitialAdBreakInfo(interstitial: Interstitial, adBreakIndex: Int): Map { + return mapOf( + ConvivaSdkConstants.POD_POSITION to calculateInterstitialAdBreakPosition(interstitial), + ConvivaSdkConstants.POD_DURATION to (interstitial.duration?.toInt() ?: 0), + ConvivaSdkConstants.POD_INDEX to adBreakIndex + ) +} + fun calculateConvivaOptions(config: ConvivaConfiguration): Map = buildMap { // No need to set GATEWAY_URL and LOG_LEVEL settings for your production release. // The Conviva SDK provides the default values for production From a3632b7e96fad5e5a9c4c0a2319a192809ef8dc8 Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 17 Aug 2026 05:49:59 +0000 Subject: [PATCH 3/3] Ignore interstitial errors for ad breaks in the past --- .../analytics/conviva/ads/AdReporter.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt index 5dcc8928..d408d1cd 100644 --- a/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt +++ b/connectors/analytics/conviva/src/main/java/com/theoplayer/android/connector/analytics/conviva/ads/AdReporter.kt @@ -10,6 +10,7 @@ import com.theoplayer.android.api.ads.AdBreak import com.theoplayer.android.api.ads.ima.GoogleImaAd import com.theoplayer.android.api.ads.ima.GoogleImaAdEvent import com.theoplayer.android.api.ads.ima.GoogleImaAdEventType +import com.theoplayer.android.api.ads.theoads.Interstitial import com.theoplayer.android.api.ads.theoads.InterstitialType import com.theoplayer.android.api.ads.theoads.TheoAdsIntegration import com.theoplayer.android.api.ads.theoads.event.InterstitialErrorEvent @@ -386,9 +387,23 @@ class AdReporter( * returns an empty VAST response. In that case no ad break or ad events are dispatched, so report * the attempted ad break as a failed ad to keep Conviva's ad attempt and fill rate metrics correct. */ + /** + * Whether the interstitial's ad break lies entirely behind the current time, for example a break in + * the DVR window of a live stream when tuning in. Such breaks can report an error without ever having + * been an actual ad attempt, so they should not be reported as failed ads. + */ + private fun isPastInterstitial(interstitial: Interstitial): Boolean { + val startTime = interstitial.startTime + if (startTime < 0.0 || startTime.isInfinite()) { + // A post-roll is never in the past. + return false + } + return startTime + (interstitial.duration ?: 0.0) < player.currentTime + } + private fun handleInterstitialError(event: InterstitialErrorEvent) { val interstitial = event.interstitial - if (interstitial.type != InterstitialType.ADBREAK || currentAdBreak != null) { + if (interstitial.type != InterstitialType.ADBREAK || currentAdBreak != null || isPastInterstitial(interstitial)) { return }