From 5f70eb7e8b67dd77e2cf3f86b9fb4d80c5dfc364 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 2 Jul 2024 14:03:14 +0200 Subject: [PATCH 1/7] Add SSAI api to ConvivaAnalyticsIntegration --- .../conviva/ConvivaAnalyticsIntegration.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) 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 14ca8ee..1885123 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java @@ -4,6 +4,9 @@ import android.os.Handler; import android.util.Log; +import com.bitmovin.analytics.conviva.ssai.DefaultPlaybackStateProvider; +import com.bitmovin.analytics.conviva.ssai.DefaultSsaiApi; +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.AdData; @@ -36,6 +39,8 @@ public class ConvivaAnalyticsIntegration { private final ConvivaVideoAnalytics convivaVideoAnalytics; private final ConvivaAdAnalytics convivaAdAnalytics; private MetadataOverrides metadataOverrides; + private final DefaultSsaiApi ssai; + // Wrapper to extract bitmovinPlayer helper methods private final BitmovinPlayerHelper playerHelper; @@ -71,6 +76,20 @@ public ConvivaAnalyticsIntegration(Player player, ConvivaConfig config, ConvivaVideoAnalytics videoAnalytics, ConvivaAdAnalytics adAnalytics + ) { + this(player, customerKey, context, config, videoAnalytics, adAnalytics, null); + } + + /** + * For testing purposes only. + */ + ConvivaAnalyticsIntegration(Player player, + String customerKey, + Context context, + ConvivaConfig config, + ConvivaVideoAnalytics videoAnalytics, + ConvivaAdAnalytics adAnalytics, + DefaultSsaiApi ssai ) { this.bitmovinPlayer = player; this.playerHelper = new BitmovinPlayerHelper(player); @@ -96,6 +115,12 @@ public ConvivaAnalyticsIntegration(Player player, convivaAdAnalytics = adAnalytics; } + if (ssai == null) { + this.ssai = new DefaultSsaiApi(convivaVideoAnalytics, convivaAdAnalytics, new DefaultPlaybackStateProvider(player)); + } else { + this.ssai = ssai; + } + attachBitmovinEventListeners(); setUpAdAnalyticsCallback(); } From 7ea592463e2a04ef32385706dcd9907b9392d407 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 2 Jul 2024 14:03:38 +0200 Subject: [PATCH 2/7] Expose SSAI namespace on ConvivaAnalyticsIntegration --- .../analytics/conviva/ConvivaAnalyticsIntegration.java | 4 ++++ 1 file changed, 4 insertions(+) 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 1885123..1c8c6cc 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java @@ -145,6 +145,10 @@ private boolean isAdActive() { } // region public methods + public SsaiApi getSsai() { + return ssai; + } + public void sendCustomApplicationEvent(String name) { sendCustomApplicationEvent(name, new HashMap<>()); } From 8ca10ac6b8bae807fec58a0fd816bfaadc9baac1 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 2 Jul 2024 14:04:24 +0200 Subject: [PATCH 3/7] Reset SSAI state on internal session end --- .../analytics/conviva/ConvivaAnalyticsIntegration.java | 3 ++- 1 file changed, 2 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 1c8c6cc..cc130c7 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java @@ -141,7 +141,7 @@ public void update(String s) { } private boolean isAdActive() { - return bitmovinPlayer.isAd(); + return bitmovinPlayer.isAd() || ssai.isAdBreakActive(); } // region public methods @@ -389,6 +389,7 @@ private void buildDynamicContentMetadata() { } private void internalEndSession() { + ssai.reset(); contentMetadataBuilder.reset(); if (!isSessionActive) { return; From 1bd6baa721efcc31029c4b865375572c642045d3 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 2 Jul 2024 14:04:52 +0200 Subject: [PATCH 4/7] Report stream error as ad error in case of active SSAI ad --- .../analytics/conviva/ConvivaAnalyticsIntegration.java | 3 +++ 1 file changed, 3 insertions(+) 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 cc130c7..98137b0 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java @@ -516,6 +516,9 @@ public void onEvent(SourceEvent.Error event) { private void handleError(String message) { ConvivaSdkConstants.ErrorSeverity severity = ConvivaSdkConstants.ErrorSeverity.FATAL; + if (ssai.isAdBreakActive()) { + convivaAdAnalytics.reportAdError(message, severity); + } convivaVideoAnalytics.reportPlaybackError(message, severity); internalEndSession(); } From 77ab16e631ad93a96e36eb404d550cbdb6473e57 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 2 Jul 2024 14:05:13 +0200 Subject: [PATCH 5/7] Add first instrumentation test --- .../analytics/conviva/testapp/SsaiTests.kt | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/SsaiTests.kt diff --git a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/SsaiTests.kt b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/SsaiTests.kt new file mode 100644 index 0000000..50b94a5 --- /dev/null +++ b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/SsaiTests.kt @@ -0,0 +1,118 @@ +package com.bitmovin.analytics.conviva.testapp + +import android.os.Handler +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.bitmovin.analytics.conviva.ConvivaAnalyticsIntegration +import com.bitmovin.analytics.conviva.ConvivaConfig +import com.bitmovin.analytics.conviva.MetadataOverrides +import com.bitmovin.analytics.conviva.ssai.SsaiApi +import com.bitmovin.analytics.conviva.testapp.framework.BITMOVIN_PLAYER_LICENSE_KEY +import com.bitmovin.analytics.conviva.testapp.framework.CONVIVA_CUSTOMER_KEY +import com.bitmovin.analytics.conviva.testapp.framework.CONVIVA_GATEWAY_URL +import com.bitmovin.analytics.conviva.testapp.framework.Sources +import com.bitmovin.analytics.conviva.testapp.framework.expectEvent +import com.bitmovin.analytics.conviva.testapp.framework.postWaiting +import com.bitmovin.player.api.PlaybackConfig +import com.bitmovin.player.api.Player +import com.bitmovin.player.api.PlayerConfig +import com.bitmovin.player.api.analytics.AnalyticsPlayerConfig +import com.bitmovin.player.api.event.PlayerEvent +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith + + +/** + * This test class does not verify any specific behavior, but rather can be used to validate the + * integration against the [Conviva Touchstone integration test tool](https://touchstone.conviva.com/). + */ +@RunWith(AndroidJUnit4::class) +class SsaiTests { + /** + * Plays a vod stream and fakes a SSAI ad break with a single 5 seconds ad and a 1 seconds slate. + */ + @Test + fun reports_ad_analytics_for_mid_roll_ad() { + val adStart = 5.0 + val adDuration = 5.0 + val slateStart = adStart + adDuration + val slateDuration = 1.0 + + + val context = InstrumentationRegistry.getInstrumentation().targetContext + val mainHandler = Handler(context.mainLooper) + val player = mainHandler.postWaiting { + Player( + context, + PlayerConfig( + key = BITMOVIN_PLAYER_LICENSE_KEY, + playbackConfig = PlaybackConfig( + isAutoplayEnabled = true, + ), + ), + analyticsConfig = AnalyticsPlayerConfig.Disabled, + ) + } + + val integration = mainHandler.postWaiting { + 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" + } + ) + convivaAnalyticsIntegration + } + + mainHandler.postWaiting { player.load(Sources.Dash.basic) } + player.expectEvent { it.time > adStart } // play main content until ad start + + // fake ad break + mainHandler.postWaiting { + integration.ssai.reportAdBreakStarted() + integration.ssai.reportAdStarted(SsaiApi.AdInfo().apply { + duration = adDuration + isSlate = false + id = "testAdId" + title = "testAdTitle" + }) + } + + player.expectEvent { it.time > adStart + adDuration } // wait unitl ad is over + + + mainHandler.postWaiting { + integration.ssai.reportAdFinished() + integration.ssai.reportAdStarted( + SsaiApi.AdInfo().apply { + duration = slateDuration + isSlate = true + title = "testSlate" + } + ) + } + + player.expectEvent { it.time > slateStart + slateDuration } // wait for five more seconds of playback + + mainHandler.postWaiting { + integration.ssai.reportAdFinished() + integration.ssai.reportAdBreakFinished() + } + + mainHandler.postWaiting { player.destroy() } + runBlocking { delay(1000) } + } +} From aecc9864e403ee82040d138a19ac78a732aa6f81 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 2 Jul 2024 15:46:50 +0200 Subject: [PATCH 6/7] Add unit tests for SSAI in ConvivaAnalyticsIntegration --- .../ConvivaAnalyticsIntegrationTest.kt | 83 +++++++++++++++---- 1 file changed, 67 insertions(+), 16 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 703e9df..8417f41 100644 --- a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt +++ b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegrationTest.kt @@ -1,19 +1,31 @@ package com.bitmovin.analytics.conviva import android.content.Context +import android.os.Handler import android.util.Log +import com.bitmovin.analytics.conviva.ssai.DefaultSsaiApi import com.bitmovin.player.api.Player +import com.bitmovin.player.api.deficiency.PlayerErrorCode import com.bitmovin.player.api.event.Event import com.bitmovin.player.api.event.EventListener import com.bitmovin.player.api.event.PlayerEvent import com.bitmovin.player.api.event.SourceEvent import com.conviva.sdk.ConvivaAdAnalytics +import com.conviva.sdk.ConvivaSdkConstants import com.conviva.sdk.ConvivaVideoAnalytics import io.mockk.clearMocks import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.mockkConstructor import io.mockk.mockkStatic +import io.mockk.runs +import io.mockk.unmockkConstructor +import io.mockk.unmockkStatic +import io.mockk.verify import org.junit.After +import org.junit.AfterClass +import org.junit.Before import org.junit.BeforeClass import org.junit.Test import strikt.api.expectThat @@ -26,45 +38,71 @@ class ConvivaAnalyticsIntegrationTest { private val player: MockPlayer = MockPlayer(mockedPlayer) private val videoAnalytics: ConvivaVideoAnalytics = mockk(relaxed = true) private val adAnalytics: ConvivaAdAnalytics = mockk(relaxed = true) + private val ssaiApi: DefaultSsaiApi = mockk() private val context: Context = mockk() private lateinit var convivaAnalyticsIntegration: ConvivaAnalyticsIntegration - @After - fun afterTest() { - clearMocks(mockedPlayer) - } + @Before + fun beforeTest() { + with(ssaiApi) { + every { isAdBreakActive } returns false + every { reset() } just runs + } - @Test - fun `initializing subscribes to player events`() { convivaAnalyticsIntegration = ConvivaAnalyticsIntegration( player, "", context, ConvivaConfig(), videoAnalytics, - adAnalytics + adAnalytics, + ssaiApi, ) + } + + @After + fun afterTest() { + clearMocks(mockedPlayer, ssaiApi, videoAnalytics, adAnalytics) + } + @Test + fun `initializing subscribes to player events`() { expectThat(player.listeners.keys).containsExactlyInAnyOrder(attachedPlayerEvents) } @Test fun `releasing unsubscribes from all events`() { - convivaAnalyticsIntegration = ConvivaAnalyticsIntegration( - player, - "", - context, - ConvivaConfig(), - videoAnalytics, - adAnalytics - ) - convivaAnalyticsIntegration.release() expectThat(player.listeners.values.flatten()).isEmpty() } + @Test + fun `reports error to ad analytics during an SSAI ad break`() { + every { ssaiApi.isAdBreakActive } returns true + + player.listeners[PlayerEvent.Error::class]?.forEach { it(PlayerEvent.Error(PlayerErrorCode.General, "error")) } + verify { adAnalytics.reportAdError(any(), ConvivaSdkConstants.ErrorSeverity.FATAL) } + } + + @Test + fun `reports player state changes to ad analytics during an SSAI ad break`() { + every { ssaiApi.isAdBreakActive } returns true + + player.listeners[PlayerEvent.Playing::class]?.forEach { it(PlayerEvent.Playing(0.0)) } + verify { adAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.PLAYING) } + + player.listeners[PlayerEvent.Paused::class]?.forEach { it(PlayerEvent.Paused(0.0)) } + verify { adAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.PAUSED) } + + player.listeners[PlayerEvent.StallStarted::class]?.forEach { it(PlayerEvent.StallStarted()) } + verify { adAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.BUFFERING) } + + player.listeners[PlayerEvent.StallEnded::class]?.forEach { it(PlayerEvent.StallEnded()) } + verify { adAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.PLAYING) } + } + companion object { @JvmStatic @BeforeClass @@ -74,6 +112,19 @@ class ConvivaAnalyticsIntegrationTest { every { Log.d(any(), any()) } returns 0 every { Log.i(any(), any()) } returns 0 every { Log.e(any(), any()) } returns 0 + + mockkConstructor(Handler::class) + every { anyConstructed().postDelayed(any(), any()) } answers { + firstArg().run() + true + } + } + + @JvmStatic + @AfterClass + fun afterClass() { + unmockkStatic(Log::class) + unmockkConstructor(Handler::class) } } } From 24995573899f886a8b1c153871663bb8c0bcb2d9 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Wed, 3 Jul 2024 10:01:53 +0200 Subject: [PATCH 7/7] Reset potential ads and ad break on reset --- .../bitmovin/analytics/conviva/ssai/DefaultSsaiApi.java | 3 ++- .../bitmovin/analytics/conviva/ssai/DefaultSsaiApiTest.kt | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/conviva/src/main/java/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApi.java b/conviva/src/main/java/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApi.java index 7246925..cedd0b9 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApi.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApi.java @@ -35,7 +35,8 @@ public void reportAdBreakStarted() { } public void reset() { - isAdBreakActive = false; + reportAdFinished(); + reportAdBreakFinished(); } @Override diff --git a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApiTest.kt b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApiTest.kt index 61f8af6..7d82d00 100644 --- a/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApiTest.kt +++ b/conviva/src/test/kotlin/com/bitmovin/analytics/conviva/ssai/DefaultSsaiApiTest.kt @@ -51,7 +51,6 @@ class DefaultSsaiApiTest { @After fun afterTest() { clearMocks(videoAnalytics, adAnalytics, playbackStateProvider) - ssaiApi.reset() } @@ -230,10 +229,14 @@ class DefaultSsaiApiTest { } @Test - fun `sets the ad break state to false when resetting`() { + fun `ends potential ads and ad breaks when resetting`() { ssaiApi.reportAdBreakStarted() + ssaiApi.reportAdStarted(SsaiApi.AdInfo()) + ssaiApi.reset() + verify { adAnalytics.reportAdEnded() } + verify { videoAnalytics.reportAdBreakEnded() } expectThat(ssaiApi.isAdBreakActive).isFalse() }