Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wire up SSAI #73

Merged
merged 8 commits into from
Jul 3, 2024
Original file line number Diff line number Diff line change
@@ -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<PlayerEvent.TimeChanged> { 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<PlayerEvent.TimeChanged> { 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<PlayerEvent.TimeChanged> { 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) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}
Expand All @@ -116,10 +141,14 @@ public void update(String s) {
}

private boolean isAdActive() {
return bitmovinPlayer.isAd();
return bitmovinPlayer.isAd() || ssai.isAdBreakActive();
}
Comment on lines 143 to 145
Copy link
Contributor Author

@strangesource strangesource Jul 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implicitly enables tracking of

  • PLAY_HEAD_TIME
  • PLAYER_STATE
    on the ConvivaAdAnalytics as it is already tracked for client side ads.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reporting the play head time, is the current format what the conviva backend expects or do we have to convert the time to a relative timestamp since the ad(break) started? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent question. I would assume no but let me double check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there is no documentation to be found about this. Let's leave it as-is for now as it makes the most sense for SSAI ads.


// region public methods
public SsaiApi getSsai() {
return ssai;
}

public void sendCustomApplicationEvent(String name) {
sendCustomApplicationEvent(name, new HashMap<>());
}
Expand Down Expand Up @@ -360,6 +389,7 @@ private void buildDynamicContentMetadata() {
}

private void internalEndSession() {
ssai.reset();
contentMetadataBuilder.reset();
if (!isSessionActive) {
return;
Expand Down Expand Up @@ -486,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);
}
rolandkakonyi marked this conversation as resolved.
Show resolved Hide resolved
convivaVideoAnalytics.reportPlaybackError(message, severity);
internalEndSession();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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<Handler>().postDelayed(any(), any()) } answers {
firstArg<Runnable>().run()
true
}
}

@JvmStatic
@AfterClass
fun afterClass() {
unmockkStatic(Log::class)
unmockkConstructor(Handler::class)
}
}
}
Expand Down