diff --git a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/AdvertisingTests.kt b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/AdvertisingTests.kt index 479fcb7..d98a44b 100644 --- a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/AdvertisingTests.kt +++ b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/AdvertisingTests.kt @@ -90,4 +90,64 @@ class AdvertisingTests { mainHandler.postWaiting { player.destroy() } runBlocking { delay(1000) } } + + /** + * Plays a live stream with a VMAP ad that includes a pre-roll, mid-roll and post-roll ad with + * attached [ConvivaAnalyticsIntegration]. + * + * The mid-roll ad is scheduled to play after 15 seconds and must show up in Conviva's Touchstone + * with the correct ad position `MIDROLL`. + */ + @Test + fun reports_correct_ad_position_on_live_stream_with_vmap_mid_roll() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val mainHandler = Handler(context.mainLooper) + val player = mainHandler.postWaiting { + val player = Player( + context, + PlayerConfig( + key = BITMOVIN_PLAYER_LICENSE_KEY, + advertisingConfig = AdvertisingConfig( + AdItem( + AdSource( + AdSourceType.Ima, + Sources.Ads.VMAP_PREROLL_MIDROLL_POSTROLL_TAG + ) + ), + ), + playbackConfig = PlaybackConfig( + isAutoplayEnabled = true, + ), + ), + analyticsConfig = AnalyticsPlayerConfig.Disabled, + ) + val convivaAnalyticsIntegration = ConvivaAnalyticsIntegration( + player, + CONVIVA_CUSTOMER_KEY, + context, + ConvivaConfig().apply { + isDebugLoggingEnabled = true + gatewayUrl = CONVIVA_GATEWAY_URL + }, + ) + + convivaAnalyticsIntegration.updateContentMetadata( + MetadataOverrides() + .apply { + applicationName = "Bitmovin Android Conviva integration example app" + viewerId = "testViewerId" + } + ) + player + } + + mainHandler.postWaiting { player.load(Sources.Dash.basicLive) } + + // mid-roll ad break is scheduled at 15 seconds, will play immediately in case of the DASH + // live stream due to absolute time stamps. + player.expectEvent { + it.adBreak!!.scheduleTime == 15.0 + } + player.expectEvent() + } } diff --git a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/framework/Sources.kt b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/framework/Sources.kt index b39329e..f47814c 100644 --- a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/framework/Sources.kt +++ b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/framework/Sources.kt @@ -6,6 +6,10 @@ import com.bitmovin.player.api.source.SourceType object Sources { object Ads { const val VMAP_PREROLL_SINGLE_TAG = "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=" + + const val VMAP_PREROLL_MIDROLL_POSTROLL_TAG = "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpost&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=" + + const val VAST_SINGLE_LINEAR_INLINE = "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=" } object Dash { @@ -14,6 +18,12 @@ object Sources { type = SourceType.Dash, title = "Art of Motion Test Stream", ) + + val basicLive = SourceConfig( + url = "https://livesim.dashif.org/livesim2/testpic_2s/Manifest.mpd", + type = SourceType.Dash, + title = "DASH livesim Live Stream", + ) } object Hls { diff --git a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java index c72c81c..049638a 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java @@ -12,6 +12,7 @@ import com.bitmovin.analytics.conviva.ssai.SsaiApi; import com.bitmovin.player.api.Player; import com.bitmovin.player.api.advertising.Ad; +import com.bitmovin.player.api.advertising.AdBreak; import com.bitmovin.player.api.advertising.AdData; import com.bitmovin.player.api.advertising.AdSourceType; import com.bitmovin.player.api.advertising.vast.AdSystem; @@ -58,6 +59,8 @@ public Boolean getSessionActive() { private Boolean isBumper = false; private Boolean isBackgrounded = false; + @Nullable + private AdBreak activeAdBreak; public ConvivaAnalyticsIntegration(String customerKey, Context context) { this( @@ -301,6 +304,7 @@ public void resumeTracking() { /** * This should be called when the app is resumed. + * * @deprecated There is no need to call this function. This is handled in the conviva-core sdk internally. */ @Deprecated @@ -314,6 +318,7 @@ public void reportAppForegrounded() { /** * This should be called when the app is paused + * * @deprecated There is no need to call this function. This is handled in the conviva-core sdk internally. */ @Deprecated @@ -729,6 +734,7 @@ public void onEvent(PlayerEvent.AdBreakStarted adBreakStarted) { // For pre-roll ads there is no `PlayerEvent.Play` before the `PlayerEvent.AdBreakStarted` // which means we need to make sure the session is correctly initialized. ensureConvivaSessionIsCreatedAndInitialized(); + activeAdBreak = adBreakStarted.getAdBreak(); convivaVideoAnalytics.reportAdBreakStarted(ConvivaSdkConstants.AdPlayer.CONTENT, ConvivaSdkConstants.AdType.CLIENT_SIDE); } }; @@ -738,6 +744,7 @@ public void onEvent(PlayerEvent.AdBreakStarted adBreakStarted) { public void onEvent(PlayerEvent.AdBreakFinished adBreakFinished) { Log.d(TAG, "[Player Event] AdBreakFinished"); convivaVideoAnalytics.reportAdBreakEnded(); + activeAdBreak = null; } }; @@ -786,7 +793,19 @@ private Map adStartedToAdInfo(PlayerEvent.AdStarted adStartedEve adInfo.put(ConvivaSdkConstants.FRAMEWORK_NAME, "Bitmovin"); adInfo.put(ConvivaSdkConstants.FRAMEWORK_VERSION, Player.getSdkVersion()); } - adInfo.put("c3.ad.position", getAdPosition(adStartedEvent.getTimeOffset())); + + double scheduleTime; + if (activeAdBreak != null) { + scheduleTime = activeAdBreak.getScheduleTime(); + } else { + Log.w( + TAG, + "No active ad break found. Using ad start time as ad position. " + + "This may result in inaccurate ad position reporting." + ); + scheduleTime = adStartedEvent.getTimeOffset(); + } + adInfo.put("c3.ad.position", getAdPosition(scheduleTime)); adInfo.put(ConvivaSdkConstants.DURATION, adStartedEvent.getDuration()); adInfo.put(ConvivaSdkConstants.IS_LIVE, convivaVideoAnalytics.getMetadataInfo().get(ConvivaSdkConstants.IS_LIVE)); diff --git a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt index 5070a34..48347ff 100644 --- a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt +++ b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt @@ -7,6 +7,10 @@ import com.bitmovin.analytics.conviva.helper.mockLogging import com.bitmovin.analytics.conviva.helper.unmockLogging import com.bitmovin.analytics.conviva.ssai.DefaultSsaiApi import com.bitmovin.player.api.Player +import com.bitmovin.player.api.advertising.Ad +import com.bitmovin.player.api.advertising.AdBreak +import com.bitmovin.player.api.advertising.AdData +import com.bitmovin.player.api.advertising.AdSourceType import com.bitmovin.player.api.deficiency.PlayerErrorCode import com.bitmovin.player.api.deficiency.PlayerWarningCode import com.bitmovin.player.api.deficiency.SourceWarningCode @@ -16,6 +20,7 @@ import com.bitmovin.player.api.media.Quality import com.bitmovin.player.api.media.video.quality.VideoQuality import com.conviva.sdk.ConvivaAdAnalytics import com.conviva.sdk.ConvivaSdkConstants +import com.conviva.sdk.ConvivaSdkConstants.AdPosition import com.conviva.sdk.ConvivaVideoAnalytics import io.mockk.clearMocks import io.mockk.every @@ -186,6 +191,30 @@ class ConvivaAnalyticsIntegrationTest { verify(exactly = 0) { videoAnalytics.reportPlaybackError(any(), any()) } } + @Test + fun `reports CSAI ad position based on last ad break schedule time`() { + player.listeners[PlayerEvent.AdBreakStarted::class]?.forEach { + it(createAdBreakStartedEvent(10.0)) + } + + verify { videoAnalytics.reportAdBreakStarted(any(), any()) } + player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD_STARTED_EVENT) } + verify { + adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.MIDROLL }) + } + + player.listeners[PlayerEvent.AdBreakStarted::class]?.forEach { + it(createAdBreakStartedEvent(0.0)) + } + + verify { videoAnalytics.reportAdBreakStarted(any(), any()) } + player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD_STARTED_EVENT) } + verify { + adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.PREROLL }) + } + + } + companion object { @JvmStatic @BeforeClass @@ -235,3 +264,23 @@ private val attachedPlayerEvents = listOf( SourceEvent.Error::class, SourceEvent.Warning::class, ) + +private val TEST_AD_STARTED_EVENT = PlayerEvent.AdStarted( + clientType = AdSourceType.Ima, + clickThroughUrl = "clickThroughUrl", + duration = 10.0, + timeOffset = 10.0, + position = "0.0", + skipOffset = 10.0, + ad = mockk(relaxed = true), + indexInQueue = 0, +) + +private fun createAdBreakStartedEvent(scheduleTime: Double): PlayerEvent.AdBreakStarted { + val adBreakStarted = PlayerEvent.AdBreakStarted( + adBreak = mockk { + every { this@mockk.scheduleTime } returns scheduleTime + } + ) + return adBreakStarted +}