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

Fix vmap mid roll ad position reporting #83

Merged
merged 8 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
Copy link

Choose a reason for hiding this comment

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

shouldn't callAndExpectEvent be used instead? The AdBreak start could be missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, in theory this could be cary. Unfortunately the test helpers seem to have an issue that cause a deadlock when loading in the callAndExpectEvent. 🙈 I'll keep it as-is. We should have a look at the test helpers at some point.

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,31 @@ class ConvivaAnalyticsIntegrationTest {
verify(exactly = 0) { videoAnalytics.reportPlaybackError(any(), any()) }
}

@Test
fun `reports CSAI ad position based on last ad break schedule time`() {

strangesource marked this conversation as resolved.
Show resolved Hide resolved
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(00.0))
strangesource marked this conversation as resolved.
Show resolved Hide resolved
}

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 })
Copy link

Choose a reason for hiding this comment

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

I didn't know this match helper. Nice!

}

}

companion object {
@JvmStatic
@BeforeClass
Expand Down Expand Up @@ -235,3 +265,49 @@ 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 = object : Ad {
override val clickThroughUrl: String?
get() = "clickThroughUrl"
override val data: AdData?
get() = null
override val height: Int
get() = 100
override val id: String?
get() = null
override val isLinear: Boolean
get() = true
override val mediaFileUrl: String?
get() = null
override val width: Int
get() = 200
Copy link

@krocard krocard Sep 24, 2024

Choose a reason for hiding this comment

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

Super nit. Why implement this though getters rather than fields?

Suggested change
ad = object : Ad {
override val clickThroughUrl: String?
get() = "clickThroughUrl"
override val data: AdData?
get() = null
override val height: Int
get() = 100
override val id: String?
get() = null
override val isLinear: Boolean
get() = true
override val mediaFileUrl: String?
get() = null
override val width: Int
get() = 200
ad = object : Ad {
override val clickThroughUrl = "clickThroughUrl"
override val data = null
override val height = 100
override val id = null
override val isLinear = true
override val mediaFileUrl = null
override val width = 200

Copy link
Contributor Author

Choose a reason for hiding this comment

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

7ee0bd3 I went for a mock to make it easier to read.


override fun clickThroughUrlOpened() {}
},
indexInQueue = 0,
)

private fun createAdBreakStartedEvent(scheduleTime: Double): PlayerEvent.AdBreakStarted {
val adBreakStarted = PlayerEvent.AdBreakStarted(
adBreak = object : AdBreak {
override val ads: List<Ad>
get() = emptyList()
override val id: String
get() = ""
override val replaceContentDuration: Double?
get() = null
override val scheduleTime: Double
get() {
return scheduleTime
}
}
)
return adBreakStarted
}