diff --git a/app/build.gradle b/app/build.gradle index 405b4e34..36e0eeaa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,9 @@ dependencies { implementation libs.androidx.compose.ui.tooling.preview debugImplementation libs.androidx.compose.ui.tooling + implementation "ch.acra:acra-http:5.7.0" + implementation fileTree(dir: './libs/GemiusSDK_2.0.8.aar', include: ['*.aar', '*.jar'], exclude: []) + implementation "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion" implementation "com.theoplayer.theoplayer-sdk-android:integration-ads-ima:$sdkVersion" implementation libs.theoplayer.android.ui @@ -69,6 +72,8 @@ dependencies { implementation project(':connectors:analytics:comscore') implementation libs.comscore + implementation project(":connectors:analytics:gemius") + implementation project(':connectors:yospace') implementation libs.yospace diff --git a/app/libs/.gitignore b/app/libs/.gitignore new file mode 100644 index 00000000..6d81fcdb --- /dev/null +++ b/app/libs/.gitignore @@ -0,0 +1 @@ +*.aar diff --git a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt index 29778b9e..35e81eb5 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import com.gemius.sdk.stream.ProgramData import com.theoplayer.android.api.THEOplayerConfig import com.theoplayer.android.api.THEOplayerView import com.theoplayer.android.api.ads.LinearAd @@ -50,7 +51,9 @@ import com.theoplayer.android.connector.analytics.comscore.ComscoreMediaType import com.theoplayer.android.connector.analytics.comscore.ComscoreMetaData import com.theoplayer.android.connector.analytics.conviva.ConvivaConfiguration import com.theoplayer.android.connector.analytics.conviva.ConvivaConnector +import com.theoplayer.android.connector.analytics.gemius.GemiusConfiguration import com.theoplayer.android.connector.analytics.nielsen.NielsenConnector +import com.theoplayer.android.connector.analytics.gemius.GemiusConnector import com.theoplayer.android.connector.uplynk.SkippedAdStrategy import com.theoplayer.android.connector.uplynk.UplynkConfiguration import com.theoplayer.android.connector.uplynk.UplynkConnector @@ -74,6 +77,7 @@ class MainActivity : ComponentActivity() { private lateinit var comscoreConnector: ComscoreConnector private lateinit var yospaceConnector: YospaceConnector private lateinit var uplynkConnector: UplynkConnector + private lateinit var gemiusConnector: GemiusConnector override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -85,6 +89,7 @@ class MainActivity : ComponentActivity() { setupNielsen() setupYospace() setupUplynk() + setupGemius() setupAdListeners() setContent { @@ -184,6 +189,26 @@ class MainActivity : ComponentActivity() { ) } + private fun setupGemius() { + val gemiusId = "your_gemius_id" + val hitCollectorHost = "your_hit_collector_host" + val gemiusConfiguration = GemiusConfiguration( + "Demo", + "1.0", + hitCollectorHost, + gemiusId, + true, + null + ) + gemiusConnector = GemiusConnector(this,gemiusConfiguration,theoplayerView) + val programData = ProgramData() + programData.name = "testasset1" + programData.programGenre = 1 + programData.programSeason = "1" + programData.programProducer = "Someone" + gemiusConnector.update("test asset 1", programData) + } + private fun setupNielsen() { val appId = "your_nielsen_app_id" nielsenConnector = NielsenConnector(applicationContext, theoplayerView.player, appId, true) diff --git a/connectors/analytics/gemius/.gitignore b/connectors/analytics/gemius/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/connectors/analytics/gemius/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/connectors/analytics/gemius/README.md b/connectors/analytics/gemius/README.md new file mode 100644 index 00000000..e69de29b diff --git a/connectors/analytics/gemius/build.gradle b/connectors/analytics/gemius/build.gradle new file mode 100644 index 00000000..23ee7420 --- /dev/null +++ b/connectors/analytics/gemius/build.gradle @@ -0,0 +1,54 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace 'com.theoplayer.android.connector.analytics.gemius' + compileSdk 35 + + defaultConfig { + minSdk 21 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +// The Gemius SDK location should be set by the app and passed in a gemiusSdkDir property. +def gemiusSdkDir = rootProject.properties['gemiusSdkDir'] +if (!gemiusSdkDir) { + logger.warn("⚠️ WARNING: gemiusSdkDir not set.") +} else if (!rootProject.file(gemiusSdkDir).exists()) { + logger.warn("⚠️ WARNING: gemiusSdkDir does not exist at: ${gemiusSdkDir}") +} else { + dependencies { + compileOnly files("${rootProject.file(gemiusSdkDir)}/GemiusSDK_2.0.8.aar") + } +} + +dependencies { + + implementation libs.androidx.core.ktx + implementation libs.androidx.appcompat + implementation libs.material + compileOnly "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion" + compileOnly "com.theoplayer.theoplayer-sdk-android:integration-ads-ima:$sdkVersion" + testImplementation libs.junit + androidTestImplementation libs.androidx.test.ext.junit + androidTestImplementation libs.androidx.test.espresso.core +} \ No newline at end of file diff --git a/connectors/analytics/gemius/consumer-rules.pro b/connectors/analytics/gemius/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/connectors/analytics/gemius/proguard-rules.pro b/connectors/analytics/gemius/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/connectors/analytics/gemius/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/connectors/analytics/gemius/src/main/AndroidManifest.xml b/connectors/analytics/gemius/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/connectors/analytics/gemius/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusAdapter.kt b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusAdapter.kt new file mode 100644 index 00000000..6d629998 --- /dev/null +++ b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusAdapter.kt @@ -0,0 +1,404 @@ +package com.theoplayer.android.connector.analytics.gemius + +import android.content.Context +import android.util.Log +import com.gemius.sdk.Config +import com.gemius.sdk.stream.AdData +import com.gemius.sdk.stream.EventAdData +import com.gemius.sdk.stream.EventProgramData +import com.theoplayer.android.api.THEOplayerView +import com.gemius.sdk.stream.Player +import com.gemius.sdk.stream.PlayerData +import com.gemius.sdk.stream.ProgramData +import com.theoplayer.android.api.ads.Ad +import com.theoplayer.android.api.ads.LinearAd +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.ads.AdBeginEvent +import com.theoplayer.android.api.event.ads.AdBreakBeginEvent +import com.theoplayer.android.api.event.ads.AdBreakEndEvent +import com.theoplayer.android.api.event.ads.AdEndEvent +import com.theoplayer.android.api.event.ads.AdSkipEvent +import com.theoplayer.android.api.event.ads.AdsEventTypes +import com.theoplayer.android.api.event.player.EndedEvent +import com.theoplayer.android.api.event.player.ErrorEvent +import com.theoplayer.android.api.event.player.PauseEvent +import com.theoplayer.android.api.event.player.PlayerEventTypes +import com.theoplayer.android.api.event.player.PlayingEvent +import com.theoplayer.android.api.event.player.SeekingEvent +import com.theoplayer.android.api.event.player.SourceChangeEvent +import com.theoplayer.android.api.event.player.VolumeChangeEvent +import com.theoplayer.android.api.event.player.WaitingEvent +import com.theoplayer.android.api.event.track.mediatrack.audio.QualityChangedEvent +import com.theoplayer.android.api.event.track.mediatrack.video.VideoTrackEventTypes +import com.theoplayer.android.api.event.track.mediatrack.video.list.AddTrackEvent +import com.theoplayer.android.api.event.track.mediatrack.video.list.RemoveTrackEvent +import com.theoplayer.android.api.event.track.mediatrack.video.list.VideoTrackListEventTypes +import com.theoplayer.android.api.player.track.mediatrack.quality.VideoQuality +import com.theoplayer.android.connector.analytics.gemius.Utils.describeEvent + +const val PLAYER_ID = "THEOplayer" +const val TAG = "GemiusConnector" + +const val GEMIUS_SDK_LOGS = true +const val PLAYER_EVENTS_LOGS = true +const val INTEGRATION_LOGS = true + +class GemiusAdapter( + context: Context, + private val configuration: GemiusConfiguration, + private val playerView: THEOplayerView, + private val adProcessor: AdProcessor? +) { + private val gemiusPlayer: Player? + + private var programId: String? = null + private var programData: ProgramData? = null + + private var partCount: Int = 1 + private var adCount: Int = 1 + private var currentAd: Ad? = null + + private val onSourceChange: EventListener + private val onFirstPlaying: EventListener +// private val onPlay: EventListener + private val onPause: EventListener + private val onWaiting: EventListener + private val onSeeking: EventListener + private val onError: EventListener + private val onEnded: EventListener + private val onVolumeChange: EventListener + + private val onAddVideoTrack: EventListener + private val onRemoveVideoTrack: EventListener + private val onVideoQualityChanged: EventListener> + + private val onAdBreakBegin: EventListener + private val onAdBegin: EventListener + private val onAdEnd: EventListener + private val onAdSkip: EventListener + private val onAdBreakEnded: EventListener + + init { + val playerData = PlayerData() + playerData.resolution = "${playerView.width}x${playerView.height}" + playerData.volume = computeVolume() + gemiusPlayer = Player(PLAYER_ID, configuration.hitCollectorHost, configuration.gemiusId, playerData) + gemiusPlayer.setContext(context) + Config.setLoggingEnabled(configuration.debug && GEMIUS_SDK_LOGS) + + onSourceChange = EventListener { event -> handleSourceChange(event) } + onFirstPlaying = EventListener { event -> handleFirstPlaying(event) } +// onPlay = EventListener { event -> handlePlay(event) } + onPause = EventListener { event -> handlePause(event) } + onWaiting = EventListener { event -> handleWaiting(event) } + onSeeking = EventListener { event -> handleSeeking(event) } + onError = EventListener { event -> handleError(event) } + onEnded = EventListener { event -> handleEnded(event) } + onVolumeChange = EventListener { event -> handleVolumeChange(event) } + onAddVideoTrack = EventListener { event -> handleAddVideoTrack(event) } + onRemoveVideoTrack = EventListener { event -> handleRemoveVideoTrack(event) } + onVideoQualityChanged = EventListener { event -> handleVideoQualityChanged(event) } + onAdBreakBegin = EventListener { event -> handleAdBreakBegin(event) } + onAdBegin = EventListener { event -> handleAdBegin(event) } + onAdEnd = EventListener { event -> handleAdEnd(event) } + onAdSkip = EventListener { event -> handleAdSkip(event) } + onAdBreakEnded = EventListener { event -> handleAdBreakEnded(event) } + + addEventListeners() + } + + fun update(programId: String, programData: ProgramData) { + this.programId = programId + this.programData = programData + } + + fun destroy() { + removeEventListeners() + } + + private fun computeVolume(): Int { + return if (playerView.player.isMuted) -1 else (playerView.player.volume * 100).toInt() + } + + private fun addEventListeners() { + playerView.player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) +// playerView.player.addEventListener(PlayerEventTypes.PLAY, onPlay) + playerView.player.addEventListener(PlayerEventTypes.PAUSE, onPause) + playerView.player.addEventListener(PlayerEventTypes.WAITING, onWaiting) + playerView.player.addEventListener(PlayerEventTypes.SEEKING, onSeeking) + playerView.player.addEventListener(PlayerEventTypes.ERROR, onError) + playerView.player.addEventListener(PlayerEventTypes.ENDED, onEnded) + playerView.player.addEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange) + playerView.player.videoTracks.addEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) + playerView.player.videoTracks.addEventListener(VideoTrackListEventTypes.REMOVETRACK, onRemoveVideoTrack) + playerView.player.ads.addEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) + playerView.player.ads.addEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) + playerView.player.ads.addEventListener(AdsEventTypes.AD_END, onAdEnd) + playerView.player.ads.addEventListener(AdsEventTypes.AD_SKIP, onAdSkip) + playerView.player.ads.addEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnded) + } + + private fun removeEventListeners() { + playerView.player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) +// playerView.player.removeEventListener(PlayerEventTypes.PLAY, onPlay) + playerView.player.removeEventListener(PlayerEventTypes.PAUSE, onPause) + playerView.player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) + playerView.player.removeEventListener(PlayerEventTypes.SEEKING, onSeeking) + playerView.player.removeEventListener(PlayerEventTypes.ERROR, onError) + playerView.player.removeEventListener(PlayerEventTypes.ENDED, onEnded) + playerView.player.removeEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange) + playerView.player.videoTracks.removeEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) + playerView.player.videoTracks.removeEventListener(VideoTrackListEventTypes.REMOVETRACK, onRemoveVideoTrack) + playerView.player.ads.removeEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) + playerView.player.ads.removeEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) + playerView.player.ads.removeEventListener(AdsEventTypes.AD_END, onAdEnd) + playerView.player.ads.removeEventListener(AdsEventTypes.AD_SKIP, onAdSkip) + playerView.player.ads.removeEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnded) + } + + private fun handleSourceChange(event: SourceChangeEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: source = ${event.source.toString()}") + } + partCount = 1 + currentAd = null + val programId = this.programId ?: return + val programData = this.programData ?: return + + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: NEW PROGRAM - id: $programId - data: $programData") + } + gemiusPlayer?.newProgram(programId,programData) + playerView.player.removeEventListener(PlayerEventTypes.PLAYING,onFirstPlaying) + playerView.player.addEventListener(PlayerEventTypes.PLAYING,onFirstPlaying) + } + private fun handleFirstPlaying(event: PlayingEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: currentTime = ${event.currentTime}") + } + val computedVolume = computeVolume() + val programId = programId ?: return + currentAd?.let { ad -> + val adId = ad.id + val adBreak = ad.adBreak ?: return + val offset = adBreak.timeOffset + val adEventData = EventAdData() + adEventData.volume = computedVolume + adEventData.breakSize = adBreak.ads.size + if (ad is LinearAd) adEventData.adDuration = ad.duration + adEventData.adPosition = adCount + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: AD EVENT (${describeEvent(Player.EventType.PLAY)}) - programId: $programId - adId: $adId - offset: $offset") + } + gemiusPlayer?.adEvent(programId, adId, offset, Player.EventType.PLAY, adEventData) + } ?: run { + if (hasPrerollScheduled()) return + val player = playerView.player + val currentQuality = player.videoTracks.first { track -> track.isEnabled }.activeQuality + val programEventData = EventProgramData() + programEventData.volume = computedVolume + programEventData.programDuration = player.duration.toInt() + programEventData.partID = partCount + programEventData.autoPlay = player.isAutoplay + if (currentQuality!= null) programEventData.quality = "${currentQuality.width}x${currentQuality.height}" + val currentTime = player.currentTime.toInt() + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: PROGRAM EVENT (${describeEvent(Player.EventType.PLAY)}) - programId: $programId - offset: $currentTime") + } + gemiusPlayer?.programEvent(programId, currentTime, Player.EventType.PLAY, programEventData) + } + } + private fun handlePause(event: PauseEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: currentTime = ${event.currentTime}") + } + reportBasicEvent(Player.EventType.PAUSE) + } + private fun handleWaiting(event: WaitingEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: currentTime = ${event.currentTime}") + } + reportBasicEvent(Player.EventType.BUFFER) + } + private fun handleSeeking(event: SeekingEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: currentTime = ${event.currentTime}") + } + reportBasicEvent(Player.EventType.SEEK) + } + private fun handleError(event: ErrorEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + val errorObject = event.errorObject + Log.d(TAG, "Player Event: ${event.type}: error = ${errorObject.code}: ${errorObject.message}") + } + reportBasicEvent(Player.EventType.COMPLETE) + } + private fun handleEnded(event: EndedEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: currentTime = ${event.currentTime}") + } + reportBasicEvent(Player.EventType.COMPLETE) + } + private fun handleVolumeChange(event: VolumeChangeEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: volume = ${event.volume}") + } + val computedVolume = computeVolume() + val programId = programId ?: return + currentAd?.let { ad -> + val adBreak = ad.adBreak ?: return + val adId = ad.id + val offset = adBreak.timeOffset + val adEventData = EventAdData() + adEventData.volume = computedVolume + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: AD EVENT (${describeEvent(Player.EventType.CHANGE_VOL)}) - programId: $programId - adId: $adId - offset: $offset") + } + gemiusPlayer?.adEvent(programId, ad.id, offset, Player.EventType.CHANGE_VOL, adEventData) + } ?: run { + val programEventData = EventProgramData() + programEventData.volume = computedVolume + val currentTime = playerView.player.currentTime.toInt() + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: PROGRAM EVENT (${describeEvent(Player.EventType.CHANGE_VOL)}) - programId: $programId - offset: $currentTime") + } + gemiusPlayer?.programEvent(programId, currentTime, Player.EventType.CHANGE_VOL, programEventData) + } + } + private fun handleAddVideoTrack(event: AddTrackEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event (Video Track): ${event.type}") + } + val track = event.track + track.addEventListener(VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onVideoQualityChanged) + } + private fun handleRemoveVideoTrack(event: RemoveTrackEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}") + } + val track = event.track + track.removeEventListener(VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onVideoQualityChanged) + } + private fun handleVideoQualityChanged(event: QualityChangedEvent<*,*>) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}") + } + val programId = programId ?: return + val activeQuality = (event.quality as? VideoQuality) ?: return + val height = activeQuality.height + val width = activeQuality.width + currentAd?.let { ad -> + val adBreak = ad.adBreak ?: return + val adId = ad.id + val offset = adBreak.timeOffset + val adEventData = EventAdData() + adEventData.quality = "${width}x${height}" + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: AD EVENT (${describeEvent(Player.EventType.CHANGE_QUAL)}) - programId: $programId - adId: $adId - offset: $offset") + } + gemiusPlayer?.adEvent(programId,ad.id,adBreak.timeOffset,Player.EventType.CHANGE_QUAL,adEventData) + } ?: run { + val programEventData = EventProgramData() + programEventData.quality = "${width}x${height}" + val currentTime = playerView.player.currentTime.toInt() + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: PROGRAM EVENT (${describeEvent(Player.EventType.CHANGE_QUAL)}) - programId: $programId - offset: $currentTime") + } + gemiusPlayer?.programEvent(programId,currentTime,Player.EventType.CHANGE_QUAL,programEventData) + } + } + private fun handleAdBreakBegin(event: AdBreakBeginEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: offset = ${event.adBreak.timeOffset}") + } + reportBasicEvent(Player.EventType.BREAK) + playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) + playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) + } + private fun handleAdBegin(event: AdBeginEvent) { + val ad = event.ad + currentAd = ad + val adId = ad?.id ?: return + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: id = $adId") + } + val adData = buildAdData(ad) + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: NEW AD - id: $adId - data: $adData") + } + gemiusPlayer?.newAd(adId,adData) + } + private fun handleAdEnd(event: AdEndEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: id = ${event.ad?.id}") + } + reportBasicEvent(Player.EventType.COMPLETE) + reportBasicEvent(Player.EventType.CLOSE) + adCount++ + currentAd = null + playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) + playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) + } + private fun handleAdSkip(event: AdSkipEvent) { + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: id = ${event.ad?.id}") + } + reportBasicEvent(Player.EventType.SKIP) + } + private fun handleAdBreakEnded(event: AdBreakEndEvent) { + val offset = event.adBreak.timeOffset + if (configuration.debug && PLAYER_EVENTS_LOGS) { + Log.d(TAG, "Player Event: ${event.type}: offset = $offset") + } + adCount = 1 + if (offset > 0) partCount++ + val programId = programId ?: return + val programData = programData ?: return + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: NEW PROGRAM - id: $programId - data: $programData") + } + gemiusPlayer?.newProgram(programId, programData) + playerView.player.removeEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) + if (offset == 0) playerView.player.addEventListener(PlayerEventTypes.PLAYING, onFirstPlaying) + } + private fun reportBasicEvent(eventType: Player.EventType) { + val programId = programId ?: return + currentAd?.let { ad -> + val offset = ad.adBreak?.timeOffset ?: return + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: AD EVENT (${describeEvent(eventType)}) - id: ${ad.id} - offset: $offset") + } + // docs mention null can be passed but interface prohibits + gemiusPlayer?.adEvent(programId,ad.id, offset, eventType, EventAdData()) + } ?: run { + val currentTime = playerView.player.currentTime.toInt() + if (configuration.debug && INTEGRATION_LOGS) { + Log.d(TAG, "Integration: PROGRAM EVENT (${describeEvent(eventType)}) - id: $programId - offset: $currentTime") + } + // docs mention null can be passed but interface prohibits + gemiusPlayer?.programEvent(programId, currentTime, eventType, EventProgramData()) + } + } + + private fun buildAdData(ad: Ad): AdData { + val adData = AdData() + val linearAd = ad as? LinearAd ?: return adData + adProcessor?.let { adProcessor -> + return adProcessor.apply(ad) + } ?: run { + adData.name = linearAd.id + adData.adType = AdData.AdType.BREAK + adData.adFormat = 1 // 1 = VIDEO ; 2 = AUDIO + adData.duration = linearAd.duration + adData.quality = "${playerView.player.videoWidth}x${playerView.player.videoHeight}" + adData.resolution = "${playerView.width}x${playerView.height}" + return adData + } + } + + private fun hasPrerollScheduled(): Boolean { + return playerView.player.ads.scheduledAds.any { it.adBreak?.timeOffset == 0 } + } +} \ No newline at end of file diff --git a/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusConfiguration.kt b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusConfiguration.kt new file mode 100644 index 00000000..5d0c4b10 --- /dev/null +++ b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusConfiguration.kt @@ -0,0 +1,10 @@ +package com.theoplayer.android.connector.analytics.gemius + +data class GemiusConfiguration ( + val applicationName: String, + val applicationVersion: String, + val hitCollectorHost: String, + val gemiusId: String, + val debug: Boolean, + val adProcessor: AdProcessor? +) \ No newline at end of file diff --git a/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusConnector.kt b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusConnector.kt new file mode 100644 index 00000000..cc3c4b27 --- /dev/null +++ b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/GemiusConnector.kt @@ -0,0 +1,27 @@ +package com.theoplayer.android.connector.analytics.gemius + +import android.content.Context +import com.gemius.sdk.stream.AdData +import com.gemius.sdk.stream.ProgramData +import com.theoplayer.android.api.THEOplayerView +import com.theoplayer.android.api.ads.Ad + +interface AdProcessor { + fun apply(input: Ad): AdData +} + +class GemiusConnector( + context: Context, + configuration: GemiusConfiguration, + playerView: THEOplayerView, +) { + private val gemiusAdapter = GemiusAdapter(context,configuration,playerView, configuration.adProcessor) + + fun update(programId: String, programData: ProgramData) { + gemiusAdapter.update(programId, programData) + } + + fun destroy() { + gemiusAdapter.destroy() + } +} \ No newline at end of file diff --git a/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/Utils.kt b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/Utils.kt new file mode 100644 index 00000000..83faea0a --- /dev/null +++ b/connectors/analytics/gemius/src/main/java/com/theoplayer/android/connector/analytics/gemius/Utils.kt @@ -0,0 +1,23 @@ +package com.theoplayer.android.connector.analytics.gemius + +import com.gemius.sdk.stream.Player + +object Utils { + fun describeEvent(eventType: Player.EventType): String = when (eventType) { + Player.EventType.SKIP -> "SKIP" + Player.EventType.PLAY -> "PLAY" + Player.EventType.PAUSE -> "PAUSE" + Player.EventType.STOP -> "STOP" + Player.EventType.CLOSE -> "CLOSE" + Player.EventType.BUFFER -> "BUFFER" + Player.EventType.BREAK -> "BREAK" + Player.EventType.SEEK -> "SEEK" + Player.EventType.COMPLETE -> "COMPLETE" + Player.EventType.NEXT -> "NEXT" + Player.EventType.PREV -> "PREV" + Player.EventType.CHANGE_VOL -> "CHANGE_VOL" + Player.EventType.CHANGE_QUAL -> "CHANGE_QUAL" + Player.EventType.CHANGE_RES -> "CHANGE_RES" + else -> "UNKNOWN EVENTTYPE" + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 22f41cdc..da0a1706 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,3 +26,5 @@ android.nonFinalResIds=true groupId=com.theoplayer.android-connector sdkVersion=9.8.2 connectorVersion=9.8.2 + +gemiusSdkDir=./app/libs \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99b80574..407011ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ conviva = "4.0.43" nielsen = "9.2.0.0" comscore = "6.10.0" yospace = "3.6.7" +material = "1.12.0" [libraries] androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } @@ -52,6 +53,7 @@ conviva = { group = "com.conviva.sdk", name = "conviva-core-sdk", version.ref = nielsen = { group = "com.nielsenappsdk.global", name = "ad", version.ref = "nielsen" } comscore = { group = "com.comscore", name = "android-analytics", version.ref = "comscore" } yospace = { group = "com.yospace", name = "admanagement-sdk", version.ref = "yospace" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/settings.gradle b/settings.gradle index dcdf50c3..7aafcda8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,8 @@ include ':app' include ':connectors:analytics:comscore' include ':connectors:analytics:conviva' include ':connectors:analytics:nielsen' +include ':connectors:analytics:gemius' include ':connectors:mediasession' include ':connectors:yospace' include ':connectors:uplynk' +include ':connectors:analytics:gemius'