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