diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..586496e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,20 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + - run: ./gradlew build diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9ff38..fe07348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [Unreleased] +## 2.4.0 - 2024-06-06 +### Added +- Ad analytics for ad event reporting + +### Changed +- Updated Bitmovin Player to `3.71.0` +- Updated IMA SDK to `3.31.0` +- Updated conviva-core to `4.0.37` +- Increased minimum required `compileSdk` version to `34` +- Increased `compileSdk` and `targetSdkVersion` to `34` +- Increased `minSdkVersion` to `19` +- Ad break started and ended is now reported in `PlayerEvent.AdBreakStarted` and `PlayerEvent.AdBreakFinished` +- Updated Kotlin to `1.9.23` +- Updated Gradle wrapper to `8.2` and AGP to `8.2.2` + +### Removed +- Custom event for `AdSkipped` and `AdError`. Replaced by Conviva build in tracking + +### Fixed +- The pom file now also includes the `com.bitmovin.player` dependency which was missing before ## 2.3.0 - 2024-05-21 ### Added diff --git a/ConvivaExampleApp/build.gradle b/ConvivaExampleApp/build.gradle index 4fb65eb..340ec77 100644 --- a/ConvivaExampleApp/build.gradle +++ b/ConvivaExampleApp/build.gradle @@ -1,7 +1,8 @@ apply plugin: 'com.android.application' android { - compileSdkVersion rootProject.compileSdkVersion + compileSdk rootProject.compileSdkVersion + defaultConfig { applicationId "com.bitmovin.analytics.convivaanalyticsexample" minSdkVersion rootProject.minSdkVersion diff --git a/ConvivaExampleApp/src/main/java/com/bitmovin/analytics/convivaanalyticsexample/MainActivity.java b/ConvivaExampleApp/src/main/java/com/bitmovin/analytics/convivaanalyticsexample/MainActivity.java index 28b0616..1e39ce9 100644 --- a/ConvivaExampleApp/src/main/java/com/bitmovin/analytics/convivaanalyticsexample/MainActivity.java +++ b/ConvivaExampleApp/src/main/java/com/bitmovin/analytics/convivaanalyticsexample/MainActivity.java @@ -9,11 +9,12 @@ import android.widget.LinearLayout; import android.widget.Switch; -import com.bitmovin.analytics.conviva.ConvivaAnalyticsIntegration;; +import com.bitmovin.analytics.conviva.ConvivaAnalyticsIntegration; import com.bitmovin.analytics.conviva.ConvivaConfig; import com.bitmovin.analytics.conviva.MetadataOverrides; import com.bitmovin.player.api.Player; import com.bitmovin.player.PlayerView; +import com.bitmovin.player.api.PlayerBuilder; import com.bitmovin.player.api.source.SourceConfig; import com.bitmovin.player.api.PlayerConfig; import com.bitmovin.player.api.advertising.AdItem; @@ -63,7 +64,10 @@ protected void onCreate(Bundle savedInstanceState) { } protected void setupBitmovinPlayer() { - this.bitmovinPlayer = Player.create(this, buildPlayerConfiguration()); + this.bitmovinPlayer = new PlayerBuilder(this) + .setPlayerConfig(buildPlayerConfiguration()) + .disableAnalytics() + .build(); this.bitmovinPlayerView = new PlayerView(this, this.bitmovinPlayer); LinearLayout playerUIView = this.findViewById(R.id.bitmovinPlayerUIView); @@ -99,6 +103,8 @@ protected void setupBitmovinPlayer() { customTags.put("custom_tag", "Episode"); metadata.setCustom(customTags); + metadata.setImaSdkVersion("3.31.0"); + convivaAnalyticsIntegration.updateContentMetadata(metadata); // load source using the created source configuration diff --git a/ConvivaTestApp/build.gradle b/ConvivaTestApp/build.gradle index 6de412a..37c44a9 100644 --- a/ConvivaTestApp/build.gradle +++ b/ConvivaTestApp/build.gradle @@ -4,8 +4,7 @@ plugins { } android { - compileSdkVersion rootProject.compileSdkVersion - buildToolsVersion "30.0.3" + compileSdk rootProject.compileSdkVersion defaultConfig { applicationId "com.bitmovin.analytics.conviva.testapp" @@ -38,7 +37,7 @@ android { } dependencies { - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.6.1' implementation bitmovinPlayerDependencies.bitmovinPlayer implementation "com.google.ads.interactivemedia.v3:interactivemedia:$googleImaSdk" // only needed if ads are used: diff --git a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/AdvertisingTests.kt b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/AdvertisingTests.kt new file mode 100644 index 0000000..1d92632 --- /dev/null +++ b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/AdvertisingTests.kt @@ -0,0 +1,133 @@ +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.player.api.PlaybackConfig +import com.bitmovin.player.api.Player +import com.bitmovin.player.api.PlayerConfig +import com.bitmovin.player.api.advertising.AdItem +import com.bitmovin.player.api.advertising.AdSource +import com.bitmovin.player.api.advertising.AdSourceType +import com.bitmovin.player.api.advertising.AdvertisingConfig +import com.bitmovin.player.api.analytics.AnalyticsPlayerConfig +import com.bitmovin.player.api.event.Event +import com.bitmovin.player.api.event.EventEmitter +import com.bitmovin.player.api.event.PlayerEvent +import com.bitmovin.player.api.event.on +import com.bitmovin.player.api.source.SourceConfig +import com.bitmovin.player.api.source.SourceType +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +private const val BITMOVIN_PLAYER_LICENSE_KEY = "YOUR_LICENSE_KEY" +private const val CONVIVA_CUSTOMER_KEY = "YOUR-CUSTOMER-KEY" +private const val CONVIVA_GATEWAY_URL = "YOUR-GATEWAY-URL" + +private 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=" +private val ART_OF_MOTION_DASH = SourceConfig( + url = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd", + type = SourceType.Dash, + title = "Art of Motion Test Stream", +) + +/** + * 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 AdvertisingTests { + /** + * Plays the first 5 seconds of a stream with a VMAP ad that includes a single pre-roll with a + * attached [ConvivaAnalyticsIntegration]. + * Ad playback is paused and resumed to test for according events. + */ + @Test + fun reports_ad_analytics_for_IMA_pre_roll_ad() { + 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, VMAP_PREROLL_SINGLE_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(ART_OF_MOTION_DASH) } + player.expectEvent { it.time > 5.0 } + + // pause player for a second and resume playback + mainHandler.postWaiting { player.pause() } + runBlocking { delay(1000) } + mainHandler.postWaiting { player.play() } + + // wait for the ad break to finish and play main content for five more seconds + player.expectEvent() + player.expectEvent { it.time > 5.0 } + + mainHandler.postWaiting { player.destroy() } + runBlocking { delay(1000) } + } +} + +/** + * Subscribes to an [Event] on the [Player] and suspends until the event is emitted. + * Optionally a [condition] can be provided to filter the emitted events. + */ +private inline fun EventEmitter.expectEvent( + crossinline condition: (T) -> Boolean = { true } +) = runBlocking { + suspendCoroutine { continuation -> + lateinit var action: ((T) -> Unit) + action = { + if (condition(it)) { + off(action) + continuation.resume(Unit) + } + } + on(action) + } +} + +/** + * Posts a [block] of code to the main thread and suspends until it is executed. + */ +private inline fun Handler.postWaiting(crossinline block: () -> T) = runBlocking { + suspendCoroutine { continuation -> + post { continuation.resume(block()) } + } +} diff --git a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/TestBase.kt b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/TestBase.kt index d426cb6..3ff04c8 100644 --- a/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/TestBase.kt +++ b/ConvivaTestApp/src/androidTest/java/com/bitmovin/analytics/conviva/testapp/TestBase.kt @@ -32,11 +32,22 @@ open class TestBase { ) as HashMap val CUSTOM_ERROR_MESSAGE = "CUSTOM_ERROR_MESSAGE" - val DEFAULT_DASH_VOD_SOURCE = Source.create(SourceConfig("https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd", SourceType.Dash)) + val DEFAULT_DASH_VOD_SOURCE = Source.create( + SourceConfig( + "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd", + SourceType.Dash + ) + ) val DEFAULT_DASH_VOD_SOURCE_DURATION = 210 - val DEFAULT_DASH_LIVE_SOURCE = Source.create(SourceConfig("https://livesim.dashif.org/livesim/testpic_2s/Manifest.mpd", SourceType.Dash)) + val DEFAULT_DASH_LIVE_SOURCE = Source.create( + SourceConfig( + "https://livesim.dashif.org/livesim/testpic_2s/Manifest.mpd", + SourceType.Dash + ) + ) val DEFAULT_DASH_LIVE_SOURCE_DURATION = 0 - val INVALID_DASH_VOD_SOURCE = Source.create(SourceConfig("https://bitmovin.com", SourceType.Dash)) + val INVALID_DASH_VOD_SOURCE = + Source.create(SourceConfig("https://bitmovin.com", SourceType.Dash)) val PLAYER_TYPE = "Bitmovin Player Android" val TIMEOUT = 4000 @@ -72,22 +83,25 @@ open class TestBase { mockConvivaAnalyticsObject() } - fun tearDownMocks () { + fun tearDownMocks() { videoAnalyticsMock?.let { unmockkObject(it) } } - fun createConvivaAnalyticsObject(activity: MainActivity) : ConvivaAnalyticsIntegration { + fun createConvivaAnalyticsObject(activity: MainActivity): ConvivaAnalyticsIntegration { // Setup mocks and create ConvivaAnalytics using mock objects ConvivaAnalytics.init(activity.applicationContext, "test") setupMocks(activity.applicationContext) - return spyk(ConvivaAnalyticsIntegration( + return spyk( + ConvivaAnalyticsIntegration( activity.bitmovinPlayer, "test", activity.applicationContext, - convivaConfig, videoAnalyticsMock)) + convivaConfig, videoAnalyticsMock + ) + ) } - fun defaultMetadataOverrides() : MetadataOverrides { + fun defaultMetadataOverrides(): MetadataOverrides { val metadata = MetadataOverrides() metadata.applicationName = "Bitmovin Android Conviva test app" metadata.viewerId = "awesomeViewerId" @@ -100,7 +114,7 @@ open class TestBase { streamType: ConvivaSdkConstants.StreamType? = null, duration: Int? = null, customTags: Map? = null - ) : MetadataOverrides { + ): MetadataOverrides { val metadata = MetadataOverrides() metadata.applicationName = "Bitmovin Android Conviva test app" metadata.viewerId = "awesomeViewerId" @@ -125,7 +139,7 @@ open class TestBase { duration: Int, overrideMetadata: MetadataOverrides, overrideCustom: Boolean = false - ) : MutableMap { + ): MutableMap { val contentInfo = mutableMapOf() contentInfo[ConvivaSdkConstants.PLAYER_NAME] = overrideMetadata.applicationName contentInfo[ConvivaSdkConstants.VIEWER_ID] = overrideMetadata.viewerId @@ -133,43 +147,50 @@ open class TestBase { contentInfo[ConvivaSdkConstants.ENCODED_FRAMERATE] = -1 contentInfo[ConvivaSdkConstants.IS_OFFLINE_PLAYBACK] = false if (overrideCustom) { - contentInfo[ConvivaSdkConstants.STREAM_URL] = overrideMetadata.streamUrl ?: source.config.url + contentInfo[ConvivaSdkConstants.STREAM_URL] = + overrideMetadata.streamUrl ?: source.config.url val streamTypeLocal = overrideMetadata.streamType ?: streamType - contentInfo[ConvivaSdkConstants.IS_LIVE] = streamTypeLocal == ConvivaSdkConstants.StreamType.LIVE; + contentInfo[ConvivaSdkConstants.IS_LIVE] = + streamTypeLocal == ConvivaSdkConstants.StreamType.LIVE; contentInfo[ConvivaSdkConstants.DURATION] = overrideMetadata.duration ?: duration overrideMetadata.custom.forEach { (s: String, s2: String) -> contentInfo[s] = s2 } - contentInfo["streamType"] = overrideMetadata.custom["streamType"] ?: source.config.type.toString() - contentInfo["integrationVersion"] = overrideMetadata.custom["integrationVersion"] ?: com.bitmovin.analytics.conviva.BuildConfig.VERSION_NAME + contentInfo["streamType"] = + overrideMetadata.custom["streamType"] ?: source.config.type.toString() + contentInfo["integrationVersion"] = overrideMetadata.custom["integrationVersion"] + ?: com.bitmovin.analytics.conviva.BuildConfig.VERSION_NAME } else { contentInfo[ConvivaSdkConstants.STREAM_URL] = source.config.url - contentInfo[ConvivaSdkConstants.IS_LIVE] = streamType == ConvivaSdkConstants.StreamType.LIVE + contentInfo[ConvivaSdkConstants.IS_LIVE] = + streamType == ConvivaSdkConstants.StreamType.LIVE contentInfo[ConvivaSdkConstants.DURATION] = duration contentInfo["streamType"] = source.config.type.toString() - contentInfo["integrationVersion"] = com.bitmovin.analytics.conviva.BuildConfig.VERSION_NAME + contentInfo["integrationVersion"] = + com.bitmovin.analytics.conviva.BuildConfig.VERSION_NAME } return contentInfo } // TODO: All references to ContentMetadata are obsolete and we should be using Map instead - fun MockKMatcherScope.metadataEq(expectedMetadata: MetadataOverrides) = match { - it.assetName == expectedMetadata.assetName && - it.applicationName == expectedMetadata.applicationName && - it.viewerId == expectedMetadata.viewerId && - it.streamType == expectedMetadata.streamType && - it.streamUrl == expectedMetadata.streamUrl && - it.duration == expectedMetadata.duration && - it.encodedFrameRate == expectedMetadata.encodedFrameRate && - it.custom.equals(expectedMetadata.custom) - } + fun MockKMatcherScope.metadataEq(expectedMetadata: MetadataOverrides) = + match { + it.assetName == expectedMetadata.assetName && + it.applicationName == expectedMetadata.applicationName && + it.viewerId == expectedMetadata.viewerId && + it.streamType == expectedMetadata.streamType && + it.streamUrl == expectedMetadata.streamUrl && + it.duration == expectedMetadata.duration && + it.encodedFrameRate == expectedMetadata.encodedFrameRate && + it.custom.equals(expectedMetadata.custom) + } fun setupPlayerActivityForTest( autoPlay: Boolean, metadataOverrides: MetadataOverrides, adTag: String = "" - ) : ActivityScenario { + ): ActivityScenario { val launchIntent = Intent( ApplicationProvider.getApplicationContext(), MainActivity::class.java @@ -207,13 +228,17 @@ open class TestBase { } // Implicit session tests do not actually initialize the session until play starts, in the new SDK - fun verifySessionInitialization(activityScenario: ActivityScenario, metadata: MetadataOverrides?, implicit: Boolean = false) { + fun verifySessionInitialization( + activityScenario: ActivityScenario, + metadata: MetadataOverrides?, + implicit: Boolean = false + ) { activityScenario.onActivity { _: MainActivity -> verifyOrder { - if(metadata != null) { + if (metadata != null) { convivaAnalyticsIntegration?.updateContentMetadata(metadata) } - if(!implicit) { + if (!implicit) { convivaAnalyticsIntegration?.initializeSession() if (metadata != null) { @@ -240,7 +265,10 @@ open class TestBase { fun verifyPlaying(activityScenario: ActivityScenario) { activityScenario.onActivity { activity: MainActivity -> verify { - videoAnalyticsMock?.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.PLAYING); + videoAnalyticsMock?.reportPlaybackMetric( + ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, + ConvivaSdkConstants.PlayerState.PLAYING + ); } } } @@ -268,18 +296,23 @@ open class TestBase { metadata.assetName = rawMetadata[ConvivaSdkConstants.ASSET_NAME] as String? metadata.applicationName = rawMetadata[ConvivaSdkConstants.PLAYER_NAME] as String? metadata.viewerId = rawMetadata[ConvivaSdkConstants.VIEWER_ID] as String? - metadata.streamType = if (rawMetadata[ConvivaSdkConstants.IS_LIVE] as Boolean) ConvivaSdkConstants.StreamType.LIVE else ConvivaSdkConstants.StreamType.VOD + metadata.streamType = + if (rawMetadata[ConvivaSdkConstants.IS_LIVE] as Boolean) ConvivaSdkConstants.StreamType.LIVE else ConvivaSdkConstants.StreamType.VOD metadata.streamUrl = rawMetadata[ConvivaSdkConstants.STREAM_URL] as String? metadata.duration = rawMetadata[ConvivaSdkConstants.DURATION] as Int? - metadata.encodedFrameRate = rawMetadata[ConvivaSdkConstants.ENCODED_FRAMERATE] as Int? + metadata.encodedFrameRate = + rawMetadata[ConvivaSdkConstants.ENCODED_FRAMERATE] as Int? - convivaAnalyticsIntegration?.updateContentMetadata(metadataEq ( metadata )) + convivaAnalyticsIntegration?.updateContentMetadata(metadataEq(metadata)) } verify(atLeast = 1) { - videoAnalyticsMock?.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.PLAYING); + videoAnalyticsMock?.reportPlaybackMetric( + ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, + ConvivaSdkConstants.PlayerState.PLAYING + ); } } } diff --git a/README.md b/README.md index fdd4f4e..82f8409 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ allprojects { And these lines to your main project ``` dependencies { - implementation 'com.conviva.sdk:conviva-core-sdk:4.0.35' // <-- conviva sdk + implementation 'com.conviva.sdk:conviva-core-sdk:4.0.37' // <-- conviva sdk implementation 'com.bitmovin.analytics:conviva:2.3.0' } ``` diff --git a/build.gradle b/build.gradle index 9933c45..3022578 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.7.0" + ext.kotlin_version = "1.9.23" repositories { mavenCentral() google() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.24.2" + classpath 'com.android.tools.build:gradle:8.2.2' + classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.1" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -17,9 +17,9 @@ buildscript { } ext { - minSdkVersion = 16 - targetSdkVersion = 33 - compileSdkVersion = 33 + minSdkVersion = 19 + targetSdkVersion = 34 + compileSdkVersion = 34 } // Load dependencies diff --git a/conviva/build.gradle b/conviva/build.gradle index 966d0eb..a8e4722 100644 --- a/conviva/build.gradle +++ b/conviva/build.gradle @@ -4,10 +4,11 @@ apply plugin: 'maven-publish' apply from: "../bitmovinpropertiesloader.gradle" def packageName = 'com.bitmovin.analytics' -def libraryVersion = '2.3.0' +def libraryVersion = '2.4.0' android { - compileSdkVersion rootProject.compileSdkVersion + namespace 'com.bitmovin.analytics.conviva' + compileSdk rootProject.compileSdkVersion defaultConfig { minSdkVersion rootProject.minSdkVersion @@ -27,44 +28,34 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - namespace 'com.bitmovin.analytics.conviva' + buildFeatures { + buildConfig true + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } } dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.2.0' + api 'com.conviva.sdk:conviva-core-sdk:4.0.37' + implementation bitmovinPlayerDependencies.bitmovinPlayer + testImplementation testingDependencies.junit androidTestImplementation testingDependencies.androidx_junit androidTestImplementation testingDependencies.androidx_espresso_core - api 'com.conviva.sdk:conviva-core-sdk:4.0.35' - implementation bitmovinPlayerDependencies.bitmovinPlayer - implementation 'org.apache.commons:commons-text:1.7' } publishing { publications { - aar(MavenPublication) { + release(MavenPublication) { groupId packageName version = libraryVersion artifactId project.getName() - // Tell maven to prepare the generated "*.aar" file for publishing - artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") - - pom.withXml { - def dependenciesNode = asNode().appendNode('dependencies') - - //Defining configuration names from which dependencies will be taken (debugCompile or releaseCompile and compile) - def configurationNames = ['api'] - - configurationNames.each { configurationName -> - configurations[configurationName].allDependencies.each { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) - } - } + afterEvaluate { + from components.release } } } @@ -81,7 +72,7 @@ artifactory { } defaults { // Tell the Artifactory Plugin which artifacts should be published to Artifactory. - publications('aar') + publications('release') publishArtifacts = true // Properties to be attached to the published artifacts. diff --git a/conviva/src/main/java/com/bitmovin/analytics/conviva/BitmovinPlayerHelper.java b/conviva/src/main/java/com/bitmovin/analytics/conviva/BitmovinPlayerHelper.java index 9dd8c4e..c1c4b6e 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/BitmovinPlayerHelper.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/BitmovinPlayerHelper.java @@ -3,7 +3,7 @@ import com.bitmovin.player.api.Player; class BitmovinPlayerHelper { - private Player player; + private final Player player; BitmovinPlayerHelper(Player player) { this.player = player; @@ -14,7 +14,7 @@ String getSdkVersionString() { } String getStreamType() { - if (player.getSource() == null || player.getSource().getConfig() == null) { + if (player.getSource() == null) { return null; } else { return player.getSource().getConfig().getType().name(); @@ -22,7 +22,7 @@ String getStreamType() { } String getStreamUrl() { - if (player.getSource() == null || player.getSource().getConfig() == null) { + if (player.getSource() == null) { return null; } else { return player.getSource().getConfig().getUrl(); diff --git a/conviva/src/main/java/com/bitmovin/analytics/conviva/ContentMetadataBuilder.java b/conviva/src/main/java/com/bitmovin/analytics/conviva/ContentMetadataBuilder.java index e4d4b6c..1ce5f3d 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ContentMetadataBuilder.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ContentMetadataBuilder.java @@ -1,8 +1,8 @@ package com.bitmovin.analytics.conviva; import android.util.Log; + import com.conviva.sdk.ConvivaSdkConstants; -import org.apache.commons.lang3.ObjectUtils; import java.util.HashMap; import java.util.Map; @@ -19,15 +19,13 @@ class ContentMetadataBuilder { private boolean playbackStarted; ContentMetadataBuilder() { - contentInfo = new HashMap(); + contentInfo = new HashMap<>(); metadata = new MetadataOverrides(); metadataOverrides = new MetadataOverrides(); } /** * This method is used for custom content metadata updates during / before a session. - * - * @param metadataOverrides */ public void setOverrides(MetadataOverrides metadataOverrides) { if (playbackStarted) { @@ -64,7 +62,7 @@ public Map build() { Integer duration = ObjectUtils.defaultIfNull( metadataOverrides.getDuration(), metadata.getDuration()); - Integer convivaDuration = duration != null ? duration : -1; + int convivaDuration = duration != null ? duration : -1; if (convivaDuration > 0) { contentInfo.put(ConvivaSdkConstants.DURATION, convivaDuration); } @@ -123,7 +121,7 @@ public Map getCustom() { // merge internal and override metadata key-value pairs // with override values having higher precedence Map customInternals = metadata.getCustom(); - Map customs = customInternals != null ? customInternals : new HashMap(); + Map customs = customInternals != null ? customInternals : new HashMap<>(); Map customOverrides = metadataOverrides.getCustom(); if (customOverrides != null) { customs.putAll(customOverrides); @@ -167,6 +165,6 @@ public void reset() { metadataOverrides = new MetadataOverrides(); metadata = new MetadataOverrides(); playbackStarted = false; - contentInfo = new HashMap(); + contentInfo = new HashMap<>(); } } diff --git a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java index 037b044..1032ad0 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ConvivaAnalyticsIntegration.java @@ -5,6 +5,11 @@ import android.util.Log; import com.bitmovin.player.api.Player; +import com.bitmovin.player.api.advertising.Ad; +import com.bitmovin.player.api.advertising.AdData; +import com.bitmovin.player.api.advertising.AdSourceType; +import com.bitmovin.player.api.advertising.vast.AdSystem; +import com.bitmovin.player.api.advertising.vast.VastAdData; import com.bitmovin.player.api.event.Event; import com.bitmovin.player.api.event.EventListener; import com.bitmovin.player.api.event.PlayerEvent; @@ -12,28 +17,30 @@ import com.bitmovin.player.api.media.video.quality.VideoQuality; import com.bitmovin.player.api.source.Source; import com.bitmovin.player.api.source.SourceConfig; +import com.conviva.sdk.ConvivaAdAnalytics; import com.conviva.sdk.ConvivaAnalytics; +import com.conviva.sdk.ConvivaExperienceAnalytics; import com.conviva.sdk.ConvivaSdkConstants; import com.conviva.sdk.ConvivaVideoAnalytics; import java.util.HashMap; +import java.util.List; import java.util.Map; public class ConvivaAnalyticsIntegration { private static final String TAG = "ConvivaAnalyticsInt"; - private Player bitmovinPlayer; - private ContentMetadataBuilder contentMetadataBuilder = new ContentMetadataBuilder(); - private ConvivaConfig config; - private ConvivaVideoAnalytics convivaVideoAnalytics; + private final Player bitmovinPlayer; + private final ContentMetadataBuilder contentMetadataBuilder = new ContentMetadataBuilder(); + private final ConvivaVideoAnalytics convivaVideoAnalytics; + private final ConvivaAdAnalytics convivaAdAnalytics; private MetadataOverrides metadataOverrides; // Wrapper to extract bitmovinPlayer helper methods - private BitmovinPlayerHelper playerHelper; + private final BitmovinPlayerHelper playerHelper; // Helper - private Boolean adStarted = false; private Boolean isSessionActive = false; private Boolean isBumper = false; private Boolean isBackgrounded = false; @@ -54,39 +61,63 @@ public ConvivaAnalyticsIntegration(Player player, Context context, ConvivaConfig config, ConvivaVideoAnalytics videoAnalytics - ) { + ) { + this(player, customerKey, context, config, videoAnalytics, null); + } + + public ConvivaAnalyticsIntegration(Player player, + String customerKey, + Context context, + ConvivaConfig config, + ConvivaVideoAnalytics videoAnalytics, + ConvivaAdAnalytics adAnalytics + ) { this.bitmovinPlayer = player; this.playerHelper = new BitmovinPlayerHelper(player); - this.config = config; - if(config.getGatewayUrl() != null || config.isDebugLoggingEnabled()) { - Map settings = new HashMap(); + Map settings = new HashMap<>(); + if (config.getGatewayUrl() != null || config.isDebugLoggingEnabled()) { if (config.getGatewayUrl() != null) { settings.put(ConvivaSdkConstants.GATEWAY_URL, config.getGatewayUrl()); } if (config.isDebugLoggingEnabled()) { settings.put(ConvivaSdkConstants.LOG_LEVEL, ConvivaSdkConstants.LogLevel.DEBUG); } - if(videoAnalytics == null) { - ConvivaAnalytics.init(context, customerKey, settings); - } - } else { - if(videoAnalytics == null) { - ConvivaAnalytics.init(context, customerKey); - } } - if(videoAnalytics != null) { + if (videoAnalytics == null) { + ConvivaAnalytics.init(context, customerKey, settings); + convivaVideoAnalytics = ConvivaAnalytics.buildVideoAnalytics(context); + } else { convivaVideoAnalytics = videoAnalytics; + } + if (adAnalytics == null) { + convivaAdAnalytics = ConvivaAnalytics.buildAdAnalytics(context, convivaVideoAnalytics); } else { - convivaVideoAnalytics = ConvivaAnalytics.buildVideoAnalytics(context); + convivaAdAnalytics = adAnalytics; } attachBitmovinEventListeners(); + setUpAdAnalyticsCallback(); + } + + private void setUpAdAnalyticsCallback() { + convivaAdAnalytics.setCallback(new ConvivaExperienceAnalytics.ICallback() { + @Override + public void update() { + if (bitmovinPlayer.isAd()) { + convivaAdAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.PLAY_HEAD_TIME, ((long) (bitmovinPlayer.getCurrentTime() * 1000))); + } + } + + @Override + public void update(String s) { + } + }); } // region public methods public void sendCustomApplicationEvent(String name) { - sendCustomApplicationEvent(name, new HashMap()); + sendCustomApplicationEvent(name, new HashMap<>()); } public void sendCustomApplicationEvent(String name, Map attributes) { @@ -95,7 +126,7 @@ public void sendCustomApplicationEvent(String name, Map attribut } public void sendCustomPlaybackEvent(String name) { - sendCustomPlaybackEvent(name, new HashMap()); + sendCustomPlaybackEvent(name, new HashMap<>()); } public void sendCustomPlaybackEvent(String name, Map attributes) { @@ -114,9 +145,7 @@ public void sendCustomPlaybackEvent(String name, Map attributes) * If no source was loaded this method will throw an error. */ public void initializeSession() throws ConvivaAnalyticsException { - if ((bitmovinPlayer.getSource() == null || - bitmovinPlayer.getSource().getConfig() == null - || bitmovinPlayer.getSource().getConfig().getTitle() == null) + if ((bitmovinPlayer.getSource() == null || bitmovinPlayer.getSource().getConfig().getTitle() == null) && this.contentMetadataBuilder.getAssetName() == null) { throw new ConvivaAnalyticsException( "AssetName is missing. Load player source (with Title) first or set assetName via updateContentMetadata" @@ -159,6 +188,7 @@ public void release() { } public void release(Boolean releaseConvivaSdk) { + convivaAdAnalytics.release(); convivaVideoAnalytics.release(); detachBitmovinEventListeners(); if (releaseConvivaSdk) { @@ -170,7 +200,7 @@ public void release(Boolean releaseConvivaSdk) { * Sends a custom deficiency event during playback to Conviva's Player Insight. If no session is active it will NOT * create one. * - * @param message Message which will be send to conviva + * @param message Message which will be send to conviva * @param severity One of FATAL or WARNING */ public void reportPlaybackDeficiency(String message, ConvivaSdkConstants.ErrorSeverity severity) { @@ -181,8 +211,8 @@ public void reportPlaybackDeficiency(String message, ConvivaSdkConstants.ErrorSe * Sends a custom deficiency event during playback to Conviva's Player Insight. If no session is active it will NOT * create one. * - * @param message Message which will be send to conviva - * @param severity One of FATAL or WARNING + * @param message Message which will be send to conviva + * @param severity One of FATAL or WARNING * @param endSession Boolean flag if session should be closed after reporting the deficiency */ public void reportPlaybackDeficiency(String message, ConvivaSdkConstants.ErrorSeverity severity, Boolean endSession) { @@ -231,7 +261,7 @@ public void reportAppForegrounded() { */ public void reportAppBackgrounded() { Log.d(TAG, "appBackgrounded"); - if(!isBackgrounded) { + if (!isBackgrounded) { ConvivaAnalytics.reportAppBackgrounded(); isBackgrounded = true; } @@ -247,7 +277,7 @@ private void ensureConvivaSessionIsCreatedAndInitialized() { } private void customEvent(Event event) { - customEvent(event, new HashMap()); + customEvent(event, new HashMap<>()); } private void customEvent(Event event, Map attributes) { @@ -258,14 +288,15 @@ private void customEvent(Event event, Map attributes) { // region Session handling private void setupPlayerStateManager() { convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.STOPPED); - Map playerInfo = new HashMap(); - playerInfo.put(ConvivaSdkConstants.FRAMEWORK_NAME,"Bitmovin Player Android"); + Map playerInfo = new HashMap<>(); + playerInfo.put(ConvivaSdkConstants.FRAMEWORK_NAME, "Bitmovin Player Android"); playerInfo.put(ConvivaSdkConstants.FRAMEWORK_VERSION, playerHelper.getSdkVersionString()); convivaVideoAnalytics.setPlayerInfo(playerInfo); + convivaAdAnalytics.setAdPlayerInfo(playerInfo); } private void internalInitializeSession() { - if(isSessionActive) { + if (isSessionActive) { return; } Log.d(TAG, "internalInitializeSession"); @@ -284,12 +315,12 @@ private void updateSession() { VideoQuality videoQuality = bitmovinPlayer.getPlaybackVideoData(); if (videoQuality != null) { int bitrate = videoQuality.getBitrate() / 1000; // in kbps - convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.RESOLUTION, videoQuality.getHeight(), videoQuality.getWidth()); - convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.BITRATE, bitrate); - convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.RENDERED_FRAMERATE, Math.round(videoQuality.getFrameRate())); + convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.RESOLUTION, videoQuality.getHeight(), videoQuality.getWidth()); + convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.BITRATE, bitrate); + convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.RENDERED_FRAMERATE, Math.round(videoQuality.getFrameRate())); } - if(isSessionActive) { + if (isSessionActive) { convivaVideoAnalytics.setContentInfo(contentMetadataBuilder.build()); } } @@ -300,13 +331,7 @@ private void createContentMetadata() { SourceConfig sourceConfig = source.getConfig(); String overriddenAssetName = metadataOverrides != null ? metadataOverrides.getAssetName() : null; - if (sourceConfig != null) { - contentMetadataBuilder.setAssetName(overriddenAssetName != null ? overriddenAssetName : sourceConfig.getTitle()); - } else { - if(overriddenAssetName != null) { - contentMetadataBuilder.setAssetName(overriddenAssetName); - } - } + contentMetadataBuilder.setAssetName(overriddenAssetName != null ? overriddenAssetName : sourceConfig.getTitle()); } this.buildDynamicContentMetadata(); } @@ -332,7 +357,7 @@ private void buildDynamicContentMetadata() { private void internalEndSession() { contentMetadataBuilder.reset(); - if(!isSessionActive) { + if (!isSessionActive) { return; } convivaVideoAnalytics.reportPlaybackEnded(); @@ -368,6 +393,8 @@ private void attachBitmovinEventListeners() { bitmovinPlayer.on(PlayerEvent.TimeShifted.class, onTimeShiftedListener); // Ad events + bitmovinPlayer.on(PlayerEvent.AdBreakStarted.class, onAdBreakStarted); + bitmovinPlayer.on(PlayerEvent.AdBreakFinished.class, onAdBreakFinished); bitmovinPlayer.on(PlayerEvent.AdStarted.class, onAdStartedListener); bitmovinPlayer.on(PlayerEvent.AdFinished.class, onAdFinishedListener); bitmovinPlayer.on(PlayerEvent.AdSkipped.class, onAdSkippedListener); @@ -404,6 +431,8 @@ private void detachBitmovinEventListeners() { bitmovinPlayer.off(PlayerEvent.TimeShifted.class, onTimeShiftedListener); // Ad events + bitmovinPlayer.off(PlayerEvent.AdBreakStarted.class, onAdBreakStarted); + bitmovinPlayer.off(PlayerEvent.AdBreakFinished.class, onAdBreakFinished); bitmovinPlayer.off(PlayerEvent.AdStarted.class, onAdStartedListener); bitmovinPlayer.off(PlayerEvent.AdFinished.class, onAdFinishedListener); bitmovinPlayer.off(PlayerEvent.AdSkipped.class, onAdSkippedListener); @@ -417,38 +446,22 @@ private void detachBitmovinEventListeners() { private synchronized void transitionState(ConvivaSdkConstants.PlayerState state) { Log.d(TAG, "Transitioning to :" + state.name()); convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, state); - } - - // region Helper - - private void trackAdEnd() { - if (!adStarted) { - // Do not track adEnd if no ad is was shown (possible if an error occurred) - return; + if (bitmovinPlayer.isAd()) { + Log.d(TAG, "Transitioning ad state to: " + state.name()); + convivaAdAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, state); } - adStarted = false; - - Log.d(TAG, "Report ad break ended"); - convivaVideoAnalytics.reportAdBreakEnded(); } - // endregion // region Listeners - private final EventListener onSourceUnloadedListener = new EventListener() { - @Override - public void onEvent(SourceEvent.Unloaded event) { + private final EventListener onSourceUnloadedListener = event -> { // The default SDK error handling is that it triggers the onSourceUnloaded before the onError event. // To track errors on Conviva we need to delay the onSourceUnloaded to ensure the onError event is // called first. // TODO: remove this once the event order is fixed on the Android SDK. - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - Log.d(TAG, "[Player Event] SourceUnloaded"); - internalEndSession(); - } + new Handler().postDelayed(() -> { + Log.d(TAG, "[Player Event] SourceUnloaded"); + internalEndSession(); }, 100); - } }; private final EventListener onPlayerErrorListener = new EventListener() { @@ -489,77 +502,53 @@ public void onEvent(SourceEvent.Warning warningEvent) { } }; - private final EventListener onMutedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.Muted event) { - Log.d(TAG, "[Player Event] Muted"); - customEvent(event); - } + private final EventListener onMutedListener = event -> { + Log.d(TAG, "[Player Event] Muted"); + customEvent(event); }; - private final EventListener onUnmutedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.Unmuted event) { - Log.d(TAG, "[Player Event] Unmuted"); - customEvent(event); - } + private final EventListener onUnmutedListener = event -> { + Log.d(TAG, "[Player Event] Unmuted"); + customEvent(event); }; // region Playback state events - private final EventListener onPlayListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.Play playEvent) { - Log.d(TAG, "[Player Event] Play"); - ensureConvivaSessionIsCreatedAndInitialized(); - updateSession(); - } + private final EventListener onPlayListener = playEvent -> { + Log.d(TAG, "[Player Event] Play"); + ensureConvivaSessionIsCreatedAndInitialized(); + updateSession(); }; - private final EventListener onPlayingListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.Playing playingEvent) { - Log.d(TAG, "[Player Event] Playing"); - contentMetadataBuilder.setPlaybackStarted(true); - transitionState(ConvivaSdkConstants.PlayerState.PLAYING); - } + private final EventListener onPlayingListener = playingEvent -> { + Log.d(TAG, "[Player Event] Playing"); + contentMetadataBuilder.setPlaybackStarted(true); + transitionState(ConvivaSdkConstants.PlayerState.PLAYING); }; - private EventListener onPausedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.Paused pausedEvent) { - // The default SDK handling is that it triggers the onPaused before the - // onError event in case of no internet connectivity. (No onPaused should be triggered) - // To ensure that no playback state change will be reported we need to delay the - // onPaused event. - // TODO: remove this once the event order is fixed on the Android SDK. - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - Log.d(TAG, "[Player Event] Paused"); - transitionState(ConvivaSdkConstants.PlayerState.PAUSED); - } - }, 100); - } + private final EventListener onPausedListener = pausedEvent -> { + // The default SDK handling is that it triggers the onPaused before the + // onError event in case of no internet connectivity. (No onPaused should be triggered) + // To ensure that no playback state change will be reported we need to delay the + // onPaused event. + // TODO: remove this once the event order is fixed on the Android SDK. + new Handler().postDelayed(() -> { + Log.d(TAG, "[Player Event] Paused"); + transitionState(ConvivaSdkConstants.PlayerState.PAUSED); + }, 100); }; - private final EventListener onPlaybackFinishedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.PlaybackFinished playbackFinishedEvent) { - Log.d(TAG, "[Player Event] PlaybackFinished"); - transitionState(ConvivaSdkConstants.PlayerState.STOPPED); - internalEndSession(); - } + private final EventListener onPlaybackFinishedListener = playbackFinishedEvent -> { + Log.d(TAG, "[Player Event] PlaybackFinished"); + transitionState(ConvivaSdkConstants.PlayerState.STOPPED); + internalEndSession(); }; - private EventListener onStallStartedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.StallStarted stallStartedEvent) { - Log.d(TAG, "[Player Event] StallStarted"); - transitionState(ConvivaSdkConstants.PlayerState.BUFFERING); - } + private final EventListener onStallStartedListener = stallStartedEvent -> { + Log.d(TAG, "[Player Event] StallStarted"); + transitionState(ConvivaSdkConstants.PlayerState.BUFFERING); }; - private EventListener onStallEndedListener = new EventListener() { + private final EventListener onStallEndedListener = new EventListener() { @Override public void onEvent(PlayerEvent.StallEnded stallEndedEvent) { // The default SDK error handling is that it triggers the onStallEnded before the @@ -567,63 +556,48 @@ public void onEvent(PlayerEvent.StallEnded stallEndedEvent) { // To track errors on Conviva we need to delay the onStallEnded to ensure no // playback state change will be reported. // TODO: remove this once the event order is fixed on the Android SDK. - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - Log.d(TAG, "[Player Event] StallEnded"); - ConvivaSdkConstants.PlayerState state = ConvivaSdkConstants.PlayerState.PLAYING; - if (bitmovinPlayer.isPaused()) { - state = ConvivaSdkConstants.PlayerState.PAUSED; - } - transitionState(state); + new Handler().postDelayed(() -> { + Log.d(TAG, "[Player Event] StallEnded"); + ConvivaSdkConstants.PlayerState state = ConvivaSdkConstants.PlayerState.PLAYING; + if (bitmovinPlayer.isPaused()) { + state = ConvivaSdkConstants.PlayerState.PAUSED; } + transitionState(state); }, 100); } }; // endregion // region Seek and Timeshift events - private EventListener onSeekListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.Seek seekEvent) { - Log.d(TAG, "[Player Event] Seek"); - setSeekStart((int) seekEvent.getTo().getTime() * 1000); - // Conviva expect notification of buffering events on seek (typically there is always buffering) - transitionState(ConvivaSdkConstants.PlayerState.BUFFERING); - } + private final EventListener onSeekListener = seekEvent -> { + Log.d(TAG, "[Player Event] Seek"); + setSeekStart((int) seekEvent.getTo().getTime() * 1000); + // Conviva expect notification of buffering events on seek (typically there is always buffering) + transitionState(ConvivaSdkConstants.PlayerState.BUFFERING); }; - private EventListener onSeekedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.Seeked seekedEvent) { - Log.d(TAG, "[Player Event] Seeked"); - setSeekEnd(); - } + private final EventListener onSeekedListener = seekedEvent -> { + Log.d(TAG, "[Player Event] Seeked"); + setSeekEnd(); }; - private EventListener onTimeShiftListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.TimeShift timeShiftEvent) { - Log.d(TAG, "[Player Event] TimeShift"); - // According to conviva it is valid to pass -1 for seeking in live streams - setSeekStart(-1); - // Conviva expect notification of buffering events on timeshift (typically there is always buffering) - transitionState(ConvivaSdkConstants.PlayerState.BUFFERING); - } + private final EventListener onTimeShiftListener = timeShiftEvent -> { + Log.d(TAG, "[Player Event] TimeShift"); + // According to conviva it is valid to pass -1 for seeking in live streams + setSeekStart(-1); + // Conviva expect notification of buffering events on timeshift (typically there is always buffering) + transitionState(ConvivaSdkConstants.PlayerState.BUFFERING); }; - private EventListener onTimeShiftedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.TimeShifted timeShiftedEvent) { - Log.d(TAG, "[Player Event] TimeShifted"); - setSeekEnd(); - } + private final EventListener onTimeShiftedListener = timeShiftedEvent -> { + Log.d(TAG, "[Player Event] TimeShifted"); + setSeekEnd(); }; private void setSeekStart(int seekTarget) { Log.d(TAG, "Sending seek start event"); convivaVideoAnalytics.reportPlaybackMetric(ConvivaSdkConstants.PLAYBACK.SEEK_STARTED, seekTarget); - }; + } public void setSeekEnd() { Log.d(TAG, "Sending seek end event"); @@ -639,51 +613,157 @@ public void setSeekEnd() { // endregion // region Ad events - private EventListener onAdStartedListener = new EventListener() { + + private final EventListener onAdBreakStarted = new EventListener() { + @Override + public void onEvent(PlayerEvent.AdBreakStarted adBreakStarted) { + Log.d(TAG, "[Player Event] 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(); + convivaVideoAnalytics.reportAdBreakStarted(ConvivaSdkConstants.AdPlayer.CONTENT, ConvivaSdkConstants.AdType.CLIENT_SIDE); + } + }; + + private final EventListener onAdBreakFinished = new EventListener() { + @Override + public void onEvent(PlayerEvent.AdBreakFinished adBreakFinished) { + Log.d(TAG, "[Player Event] AdBreakFinished"); + convivaVideoAnalytics.reportAdBreakEnded(); + } + }; + + private final EventListener onAdStartedListener = new EventListener() { @Override public void onEvent(PlayerEvent.AdStarted adStartedEvent) { Log.d(TAG, "[Player Event] AdStarted"); - adStarted = true; - convivaVideoAnalytics.reportAdBreakStarted(ConvivaSdkConstants.AdPlayer.CONTENT, ConvivaSdkConstants.AdType.CLIENT_SIDE); + Map adInfo = adStartedToAdInfo(adStartedEvent); + convivaAdAnalytics.reportAdLoaded(adInfo); + convivaAdAnalytics.reportAdStarted(adInfo); + convivaAdAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.PLAYER_STATE, ConvivaSdkConstants.PlayerState.PLAYING); + Ad ad = adStartedEvent.getAd(); + if (ad != null) { + if (ad.getWidth() != 0 && ad.getHeight() != 0) { + convivaAdAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.RESOLUTION, ad.getWidth(), ad.getHeight()); + } + AdData adData = ad.getData(); + if (adData != null && adData.getBitrate() != null) { + convivaAdAnalytics.reportAdMetric(ConvivaSdkConstants.PLAYBACK.BITRATE, adData.getBitrate()); + } + } } }; - private EventListener onAdFinishedListener = new EventListener() { + private Map adStartedToAdInfo(PlayerEvent.AdStarted adStartedEvent) { + Map adInfo = new HashMap<>(); + + adInfo.put("c3.ad.id", "NA"); + adInfo.put("c3.ad.system", "NA"); + adInfo.put("c3.ad.mediaFileApiFramework", "NA"); + adInfo.put("c3.ad.firstAdSystem", "NA"); + adInfo.put("c3.ad.firstAdId", "NA"); + adInfo.put("c3.ad.firstCreativeId", "NA"); + + adInfo.put("c3.ad.technology", "Client Side"); + if (adStartedEvent.getClientType() == AdSourceType.Ima) { + adInfo.put(ConvivaSdkConstants.FRAMEWORK_NAME, "Google IMA SDK"); + adInfo.put( + ConvivaSdkConstants.FRAMEWORK_VERSION, + metadataOverrides.getImaSdkVersion() != null ? metadataOverrides.getImaSdkVersion() : "NA" + ); + } else { + adInfo.put(ConvivaSdkConstants.FRAMEWORK_NAME, "Bitmovin"); + adInfo.put(ConvivaSdkConstants.FRAMEWORK_VERSION, playerHelper.getSdkVersionString()); + } + adInfo.put("c3.ad.position", getAdPosition(adStartedEvent.getTimeOffset())); + adInfo.put(ConvivaSdkConstants.DURATION, adStartedEvent.getDuration()); + adInfo.put(ConvivaSdkConstants.IS_LIVE, convivaVideoAnalytics.getMetadataInfo().get(ConvivaSdkConstants.IS_LIVE)); + + Ad ad = adStartedEvent.getAd(); + if (ad != null) { + if (ad.getMediaFileUrl() != null) { + adInfo.put(ConvivaSdkConstants.STREAM_URL, ad.getMediaFileUrl()); + } + if (ad.getId() != null) { + adInfo.put("c3.ad.id", ad.getId()); + } + + if (ad.getData() instanceof VastAdData) { + setVastAdMetadata((VastAdData) ad.getData(), adInfo); + } + } + return adInfo; + } + + private static void setVastAdMetadata(VastAdData vastAdData, Map adInfo) { + if (vastAdData.getAdTitle() != null) { + adInfo.put(ConvivaSdkConstants.ASSET_NAME, vastAdData.getAdTitle()); + } + if (vastAdData.getCreative() != null && vastAdData.getCreative().getId() != null) { + adInfo.put("c3.ad.creativeId", vastAdData.getCreative().getId()); + } + if (vastAdData.getAdDescription() != null) { + adInfo.put("c3.ad.description", vastAdData.getAdDescription()); + } + if (vastAdData.getAdSystem() != null) { + adInfo.put("c3.ad.system", vastAdData.getAdSystem().getName()); + } + List wrapperAdSystems = vastAdData.getWrapperAdSystems(); + if (!wrapperAdSystems.isEmpty()) { + adInfo.put("c3.ad.firstAdSystem", wrapperAdSystems.get(wrapperAdSystems.size() - 1).getName()); + } + String[] wrapperAdIds = vastAdData.getWrapperAdIds(); + if (wrapperAdIds.length != 0) { + adInfo.put("c3.ad.firstAdId", wrapperAdIds[wrapperAdIds.length - 1]); + } + List wrapperCreativeIds = vastAdData.getWrapperCreativeIds(); + if (!wrapperCreativeIds.isEmpty()) { + adInfo.put("c3.ad.firstCreativeId", wrapperCreativeIds.get(wrapperCreativeIds.size() - 1)); + } + } + + private ConvivaSdkConstants.AdPosition getAdPosition(double timeOffset) { + ConvivaSdkConstants.AdPosition adPosition = ConvivaSdkConstants.AdPosition.MIDROLL; + if (timeOffset == 0.0) { + adPosition = ConvivaSdkConstants.AdPosition.PREROLL; + } else if (timeOffset == bitmovinPlayer.getDuration()) { + adPosition = ConvivaSdkConstants.AdPosition.POSTROLL; + } + return adPosition; + } + + private final EventListener onAdFinishedListener = new EventListener() { @Override - public void onEvent(PlayerEvent.AdFinished adFinishedEvent) { + public void onEvent(PlayerEvent.AdFinished adFinished) { Log.d(TAG, "[Player Event] AdFinished"); - trackAdEnd(); + convivaAdAnalytics.reportAdEnded(); } }; - private EventListener onAdSkippedListener = new EventListener() { + private final EventListener onAdSkippedListener = new EventListener() { @Override - public void onEvent(PlayerEvent.AdSkipped adSkippedEvent) { + public void onEvent(PlayerEvent.AdSkipped adSkipped) { Log.d(TAG, "[Player Event] AdSkipped"); - customEvent(adSkippedEvent); - trackAdEnd(); + convivaAdAnalytics.reportAdSkipped(); } }; - private EventListener onAdErrorListener = new EventListener() { + private final EventListener onAdErrorListener = new EventListener() { @Override - public void onEvent(PlayerEvent.AdError adErrorEvent) { + public void onEvent(PlayerEvent.AdError adError) { Log.d(TAG, "[Player Event] AdError"); - customEvent(adErrorEvent); - trackAdEnd(); + convivaAdAnalytics.reportAdFailed(adError.getMessage()); } }; + // endregion - private EventListener onVideoPlaybackQualityChangedListener = new EventListener() { - @Override - public void onEvent(PlayerEvent.VideoPlaybackQualityChanged videoPlaybackQualityChangedEvent) { - Log.d(TAG, "[Player Event] VideoPlaybackQualityChanged"); - updateSession(); - } + private final EventListener onVideoPlaybackQualityChangedListener = videoPlaybackQualityChangedEvent -> { + Log.d(TAG, "[Player Event] VideoPlaybackQualityChanged"); + updateSession(); }; - private EventListener onTimeChangedListener = new EventListener() { + private final EventListener onTimeChangedListener = new EventListener() { @Override public void onEvent(PlayerEvent.TimeChanged timeChangedEvent) { if (bitmovinPlayer.isLive()) { diff --git a/conviva/src/main/java/com/bitmovin/analytics/conviva/MetadataOverrides.java b/conviva/src/main/java/com/bitmovin/analytics/conviva/MetadataOverrides.java index e60cf78..c0ab7e6 100644 --- a/conviva/src/main/java/com/bitmovin/analytics/conviva/MetadataOverrides.java +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/MetadataOverrides.java @@ -20,6 +20,7 @@ public class MetadataOverrides { private Integer encodedFrameRate; private String defaultResource; private String streamUrl; + private String imaSdkVersion; public String getAssetName() { return assetName; @@ -104,4 +105,16 @@ public String getStreamUrl() { public void setStreamUrl(String streamUrl) { this.streamUrl = streamUrl; } + + public String getImaSdkVersion() { + return imaSdkVersion; + } + + /** + * Set the IMA SDK version to be tracked with client side ads of type + * {@link com.bitmovin.player.api.advertising.AdSourceType#Ima}. + */ + public void setImaSdkVersion(String imaSdkVersion) { + this.imaSdkVersion = imaSdkVersion; + } } diff --git a/conviva/src/main/java/com/bitmovin/analytics/conviva/ObjectUtils.java b/conviva/src/main/java/com/bitmovin/analytics/conviva/ObjectUtils.java new file mode 100644 index 0000000..4bebd47 --- /dev/null +++ b/conviva/src/main/java/com/bitmovin/analytics/conviva/ObjectUtils.java @@ -0,0 +1,7 @@ +package com.bitmovin.analytics.conviva; + +/*package*/ class ObjectUtils { + public static T defaultIfNull(T object, T defaultValue) { + return object != null ? object : defaultValue; + } +} diff --git a/dependencies.gradle b/dependencies.gradle index 15051ac..71cd90c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ ext { - bitmovinPlayerVersion = '3.39.0' - googleImaSdk = '3.29.0' + bitmovinPlayerVersion = '3.71.0' + googleImaSdk = '3.31.0' googlePlayAdsIdentifier = '18.0.1' bitmovinPlayerDependencies = [ @@ -14,4 +14,4 @@ ext { androidx_espresso_core: 'androidx.test.espresso:espresso-core:3.5.1', mockk: 'io.mockk:mockk-android:1.12.0' ] -} \ No newline at end of file +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 83ce604..7f8f151 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Oct 12 15:36:49 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME