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..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,12 @@ 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 +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 @@ -23,9 +29,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 @@ -63,6 +71,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 +175,10 @@ class AdReporter( handleAdError() } + onInterstitialError = EventListener { event -> + handleInterstitialError(event) + } + addEventListeners() } @@ -197,6 +220,8 @@ class AdReporter( onImaContentResume ) } + + theoAds?.addEventListener(TheoAdsEventTypes.INTERSTITIAL_ERROR, onInterstitialError) } private fun removeEventListeners() { @@ -221,8 +246,22 @@ class AdReporter( onImaContentResume ) } + + 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() @@ -255,15 +294,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, ) @@ -350,6 +382,52 @@ 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. + */ + /** + * 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 || isPastInterstitial(interstitial)) { + return + } + + // Make sure the session is started + convivaHandler.maybeReportPlaybackRequested() + + adBreakCounter++ + convivaVideoAnalytics.reportAdBreakStarted( + ConvivaSdkConstants.AdPlayer.CONTENT, + ConvivaSdkConstants.AdType.SERVER_SIDE, + calculateInterstitialAdBreakInfo(interstitial, adBreakCounter) + ) + val adMetadata = collectBaseAdMetadata() + mapOf( + "c3.ad.technology" to SGAI_AD_TYPE, + 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) { 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