From 8e53f90f81cb192777955473b5acb94361889310 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 13:04:36 +0200 Subject: [PATCH 1/8] Calculate ad potition with offset from ad break --- .../conviva/ConvivaAnalyticsIntegration.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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..e5a5417 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; @@ -301,6 +302,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 +316,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 @@ -722,6 +725,8 @@ public void setSeekEnd() { // region Ad events + @Nullable + private AdBreak activeAdBreak; private final EventListener onAdBreakStarted = new EventListener() { @Override public void onEvent(PlayerEvent.AdBreakStarted adBreakStarted) { @@ -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)); From 34ca984a8c5d92ed779d86675617dafc111b7b6e Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 13:11:28 +0200 Subject: [PATCH 2/8] Add tests and new test sources --- .../conviva/testapp/AdvertisingTests.kt | 60 +++++++++++++++++++ .../conviva/testapp/framework/Sources.kt | 10 ++++ 2 files changed, 70 insertions(+) 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 { From 88825f9ef3e5fea4af1161cbf4331e30131756dc Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 13:29:37 +0200 Subject: [PATCH 3/8] Add unit tests --- .../ConvivaAnalyticsIntegrationTest.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) 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..ed19d5f 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,31 @@ 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) } + verify { + adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.MIDROLL }) + } + + player.listeners[PlayerEvent.AdBreakStarted::class]?.forEach { + it(createAdBreakStartedEvent(00.0)) + } + + verify { videoAnalytics.reportAdBreakStarted(any(), any()) } + player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD) } + verify { + adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.PREROLL }) + } + + } + companion object { @JvmStatic @BeforeClass @@ -235,3 +265,49 @@ private val attachedPlayerEvents = listOf( SourceEvent.Error::class, SourceEvent.Warning::class, ) + +private val TEST_AD = PlayerEvent.AdStarted( + clientType = AdSourceType.Ima, + clickThroughUrl = "clickThroughUrl", + duration = 10.0, + timeOffset = 10.0, + position = "0.0", + skipOffset = 10.0, + ad = object : Ad { + override val clickThroughUrl: String? + get() = "clickThroughUrl" + override val data: AdData? + get() = null + override val height: Int + get() = 100 + override val id: String? + get() = null + override val isLinear: Boolean + get() = true + override val mediaFileUrl: String? + get() = null + override val width: Int + get() = 200 + + override fun clickThroughUrlOpened() {} + }, + indexInQueue = 0, +) + +private fun createAdBreakStartedEvent(scheduleTime: Double): PlayerEvent.AdBreakStarted { + val adBreakStarted = PlayerEvent.AdBreakStarted( + adBreak = object : AdBreak { + override val ads: List + get() = emptyList() + override val id: String + get() = "" + override val replaceContentDuration: Double? + get() = null + override val scheduleTime: Double + get() { + return scheduleTime + } + } + ) + return adBreakStarted +} From 0a229a1cfcc18d6945a25fdb73cd13e87bcb11a6 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 13:37:13 +0200 Subject: [PATCH 4/8] Move field up --- .../analytics/conviva/ConvivaAnalyticsIntegration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e5a5417..049638a 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java @@ -59,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( @@ -725,8 +727,6 @@ public void setSeekEnd() { // region Ad events - @Nullable - private AdBreak activeAdBreak; private final EventListener onAdBreakStarted = new EventListener() { @Override public void onEvent(PlayerEvent.AdBreakStarted adBreakStarted) { From 6421af10f457a1d2b54654ccb056d6b56ce258f7 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 13:38:32 +0200 Subject: [PATCH 5/8] Improve naming --- .../analytics/conviva/ConvivaAnalyticsIntegrationTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 ed19d5f..1ccb46e 100644 --- a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt +++ b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt @@ -199,7 +199,7 @@ class ConvivaAnalyticsIntegrationTest { } verify { videoAnalytics.reportAdBreakStarted(any(), any()) } - player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD) } + player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD_STARTED_EVENT) } verify { adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.MIDROLL }) } @@ -209,7 +209,7 @@ class ConvivaAnalyticsIntegrationTest { } verify { videoAnalytics.reportAdBreakStarted(any(), any()) } - player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD) } + player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD_STARTED_EVENT) } verify { adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.PREROLL }) } @@ -266,7 +266,7 @@ private val attachedPlayerEvents = listOf( SourceEvent.Warning::class, ) -private val TEST_AD = PlayerEvent.AdStarted( +private val TEST_AD_STARTED_EVENT = PlayerEvent.AdStarted( clientType = AdSourceType.Ima, clickThroughUrl = "clickThroughUrl", duration = 10.0, From 7ee0bd34b39a86df99f7e5ca503fe47b2619a843 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 15:43:39 +0200 Subject: [PATCH 6/8] Use mocks instead of stubs --- .../ConvivaAnalyticsIntegrationTest.kt | 32 ++----------------- 1 file changed, 3 insertions(+), 29 deletions(-) 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 1ccb46e..374529d 100644 --- a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt +++ b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt @@ -273,40 +273,14 @@ private val TEST_AD_STARTED_EVENT = PlayerEvent.AdStarted( timeOffset = 10.0, position = "0.0", skipOffset = 10.0, - ad = object : Ad { - override val clickThroughUrl: String? - get() = "clickThroughUrl" - override val data: AdData? - get() = null - override val height: Int - get() = 100 - override val id: String? - get() = null - override val isLinear: Boolean - get() = true - override val mediaFileUrl: String? - get() = null - override val width: Int - get() = 200 - - override fun clickThroughUrlOpened() {} - }, + ad = mockk(relaxed = true), indexInQueue = 0, ) private fun createAdBreakStartedEvent(scheduleTime: Double): PlayerEvent.AdBreakStarted { val adBreakStarted = PlayerEvent.AdBreakStarted( - adBreak = object : AdBreak { - override val ads: List - get() = emptyList() - override val id: String - get() = "" - override val replaceContentDuration: Double? - get() = null - override val scheduleTime: Double - get() { - return scheduleTime - } + adBreak = mockk { + every { this@mockk.scheduleTime } returns scheduleTime } ) return adBreakStarted From 03e0dfeebdd6b783349bb559e0adf58b5aff9383 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 15:53:14 +0200 Subject: [PATCH 7/8] Update conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt Co-authored-by: Matthias Tamegger <5555332+matamegger@users.noreply.github.com> --- .../analytics/conviva/ConvivaAnalyticsIntegrationTest.kt | 1 - 1 file changed, 1 deletion(-) 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 374529d..fda4142 100644 --- a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt +++ b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt @@ -193,7 +193,6 @@ class ConvivaAnalyticsIntegrationTest { @Test fun `reports CSAI ad position based on last ad break schedule time`() { - player.listeners[PlayerEvent.AdBreakStarted::class]?.forEach { it(createAdBreakStartedEvent(10.0)) } From d91208b8a3ae5355cc318132447a476a83f8be96 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 24 Sep 2024 15:53:25 +0200 Subject: [PATCH 8/8] Update conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt Co-authored-by: Matthias Tamegger <5555332+matamegger@users.noreply.github.com> --- .../analytics/conviva/ConvivaAnalyticsIntegrationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fda4142..48347ff 100644 --- a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt +++ b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt @@ -204,7 +204,7 @@ class ConvivaAnalyticsIntegrationTest { } player.listeners[PlayerEvent.AdBreakStarted::class]?.forEach { - it(createAdBreakStartedEvent(00.0)) + it(createAdBreakStartedEvent(0.0)) } verify { videoAnalytics.reportAdBreakStarted(any(), any()) }