Skip to content

Commit

Permalink
Merge pull request #83 from bitmovin/fix-vmap-mid-roll-ad-position-re…
Browse files Browse the repository at this point in the history
…porting

Fix vmap mid roll ad position reporting
  • Loading branch information
strangesource authored Sep 24, 2024
2 parents 75bce0d + d91208b commit 3938a88
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlayerEvent.AdBreakStarted> {
it.adBreak!!.scheduleTime == 15.0
}
player.expectEvent<PlayerEvent.AdBreakFinished>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,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(
Expand Down Expand Up @@ -301,6 +304,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
Expand All @@ -314,6 +318,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
Expand Down Expand Up @@ -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);
}
};
Expand All @@ -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;
}
};

Expand Down Expand Up @@ -786,7 +793,19 @@ private Map<String, Object> 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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -186,6 +191,30 @@ 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_STARTED_EVENT) }
verify {
adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.MIDROLL })
}

player.listeners[PlayerEvent.AdBreakStarted::class]?.forEach {
it(createAdBreakStartedEvent(0.0))
}

verify { videoAnalytics.reportAdBreakStarted(any(), any()) }
player.listeners[PlayerEvent.AdStarted::class]?.forEach { it(TEST_AD_STARTED_EVENT) }
verify {
adAnalytics.reportAdStarted(match { it["c3.ad.position"] == AdPosition.PREROLL })
}

}

companion object {
@JvmStatic
@BeforeClass
Expand Down Expand Up @@ -235,3 +264,23 @@ private val attachedPlayerEvents = listOf(
SourceEvent.Error::class,
SourceEvent.Warning::class,
)

private val TEST_AD_STARTED_EVENT = PlayerEvent.AdStarted(
clientType = AdSourceType.Ima,
clickThroughUrl = "clickThroughUrl",
duration = 10.0,
timeOffset = 10.0,
position = "0.0",
skipOffset = 10.0,
ad = mockk(relaxed = true),
indexInQueue = 0,
)

private fun createAdBreakStartedEvent(scheduleTime: Double): PlayerEvent.AdBreakStarted {
val adBreakStarted = PlayerEvent.AdBreakStarted(
adBreak = mockk {
every { this@mockk.scheduleTime } returns scheduleTime
}
)
return adBreakStarted
}

0 comments on commit 3938a88

Please sign in to comment.