Skip to content

Commit

Permalink
Merge branch 'wire-up-ssai' into report-initial-ad-metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
strangesource committed Jul 3, 2024
2 parents e74ba1c + 2499557 commit 6347eb4
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 20 deletions.
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();
}

// 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);
}
convivaVideoAnalytics.reportPlaybackError(message, severity);
internalEndSession();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public void reportAdBreakStarted() {
}

public void reset() {
isAdBreakActive = false;
reportAdFinished();
reportAdBreakFinished();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
public interface SsaiApi {
/**
* Checks if a server-side ad break is currently active.
* Reports if a server-side ad break is currently active.
*
* @return <code>true</code> if a server-side ad break is active, <code>false</code> otherwise.
*/
Expand Down Expand Up @@ -35,6 +35,8 @@ public interface SsaiApi {

/**
* Reports the start of a server-side ad.
* <p>
* Has to be called after calling the <code>reportAdBreakStarted</code> method.
*
* @param adInfo Object containing metadata about the server-side ad.
*/
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
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,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()
}

Expand Down

0 comments on commit 6347eb4

Please sign in to comment.