From 653bf401e5b598dcf2f8e8c56967b8cdd4f61eee Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 22 May 2024 14:05:13 +0200 Subject: [PATCH 01/71] Add yospace module --- connectors/yospace/.gitignore | 1 + connectors/yospace/build.gradle | 36 +++++++++++++++++++ connectors/yospace/consumer-rules.pro | 0 connectors/yospace/proguard-rules.pro | 21 +++++++++++ .../yospace/src/main/AndroidManifest.xml | 3 ++ settings.gradle | 1 + 6 files changed, 62 insertions(+) create mode 100644 connectors/yospace/.gitignore create mode 100644 connectors/yospace/build.gradle create mode 100644 connectors/yospace/consumer-rules.pro create mode 100644 connectors/yospace/proguard-rules.pro create mode 100644 connectors/yospace/src/main/AndroidManifest.xml diff --git a/connectors/yospace/.gitignore b/connectors/yospace/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/connectors/yospace/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle new file mode 100644 index 00000000..7fec0097 --- /dev/null +++ b/connectors/yospace/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} +apply from: "$rootDir/connectors/publish.gradle" + +android { + namespace 'com.theoplayer.android.connector.yospace' + compileSdk 34 + + defaultConfig { + minSdk 21 + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.13.1' + compileOnly "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion" +} \ No newline at end of file diff --git a/connectors/yospace/consumer-rules.pro b/connectors/yospace/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/connectors/yospace/proguard-rules.pro b/connectors/yospace/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/connectors/yospace/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/yospace/src/main/AndroidManifest.xml b/connectors/yospace/src/main/AndroidManifest.xml new file mode 100644 index 00000000..74b7379f --- /dev/null +++ b/connectors/yospace/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4432798d..6f6043d2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,3 +22,4 @@ include ':connectors:analytics:comscore' include ':connectors:analytics:conviva' include ':connectors:analytics:nielsen' include ':connectors:mediasession' +include ':connectors:yospace' From 2f0263f06b2ff18e08cf0baaee2c07a6d4ec340b Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 22 May 2024 14:24:50 +0200 Subject: [PATCH 02/71] Add dependency on Yospace Ad Management SDK --- connectors/yospace/build.gradle | 6 ++++++ settings.gradle | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index 7fec0097..f35dd9f8 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -33,4 +33,10 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.13.1' compileOnly "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion" + compileOnly("com.yospace:admanagement-sdk") { + version { + strictly("[3.6, 4.0)") + prefer("3.6.7") + } + } } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 6f6043d2..e39afd8f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,7 +12,14 @@ dependencyResolutionManagement { google() mavenCentral() maven { url 'https://maven.theoplayer.com/releases/' } - maven { url 'https://raw.githubusercontent.com/NielsenDigitalSDK/nielsenappsdk-android/master/'} + maven { url 'https://raw.githubusercontent.com/NielsenDigitalSDK/nielsenappsdk-android/master/' } + maven { + url 'https://yospacerepo.jfrog.io/yospacerepo/android-sdk' + credentials { + username System.getenv("YOSPACE_USERNAME") + password System.getenv("YOSPACE_PASSWORD") + } + } } } From d964cf630c4c9ac8ea64d335fae1abeb853af877 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 27 May 2024 16:51:25 +0200 Subject: [PATCH 03/71] Add basic YospaceConnector --- .../connector/yospace/YospaceConnector.kt | 44 +++++++++ .../connector/yospace/YospaceListener.kt | 8 ++ .../yospace/YospaceSsaiDescription.kt | 48 +++++++++ .../yospace/internal/SourceHelpers.kt | 28 ++++++ .../yospace/internal/YospaceAdIntegration.kt | 98 +++++++++++++++++++ .../yospace/internal/YospaceResultCode.kt | 8 ++ 6 files changed, 234 insertions(+) create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceListener.kt create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SourceHelpers.kt create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceResultCode.kt diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt new file mode 100644 index 00000000..3aba5ba7 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt @@ -0,0 +1,44 @@ +package com.theoplayer.android.connector.yospace + +import com.theoplayer.android.api.ads.ServerSideAdIntegrationController +import com.theoplayer.android.api.player.Player +import com.theoplayer.android.connector.yospace.internal.YospaceAdIntegration +import java.util.concurrent.CopyOnWriteArrayList + +const val INTEGRATION_ID = "yospace" +const val TAG = "YospaceConnector" + +class YospaceConnector( + player: Player +) { + private val listeners = CopyOnWriteArrayList() + + private lateinit var integration: YospaceAdIntegration + + init { + player.ads.registerServerSideIntegration(INTEGRATION_ID, this::setupIntegration) + } + + private fun setupIntegration(controller: ServerSideAdIntegrationController): YospaceAdIntegration { + val integration = YospaceAdIntegration( + controller, + ForwardingListener() + ) + this.integration = integration + return integration + } + + fun addListener(listener: YospaceListener) { + listeners.add(listener) + } + + fun removeListener(listener: YospaceListener) { + listeners.remove(listener) + } + + private inner class ForwardingListener : YospaceListener { + override fun onSessionAvailable() { + listeners.forEach { it.onSessionAvailable() } + } + } +} diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceListener.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceListener.kt new file mode 100644 index 00000000..60cbbdb2 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceListener.kt @@ -0,0 +1,8 @@ +package com.theoplayer.android.connector.yospace + +interface YospaceListener { + /** + * Fired when a new Yospace session starts. + */ + fun onSessionAvailable() {} +} \ No newline at end of file diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt new file mode 100644 index 00000000..dae47996 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt @@ -0,0 +1,48 @@ +package com.theoplayer.android.connector.yospace + +import com.theoplayer.android.api.source.ssai.CustomSsaiDescription +import com.yospace.admanagement.Session + +/** + * The configuration for server-side ad insertion using the Yospace connector. + */ +class YospaceSsaiDescription( + /** + * The type of the requested stream. + * + * Default: [YospaceStreamType.LIVE] + */ + val streamType: YospaceStreamType = YospaceStreamType.LIVE, + /** + * Custom properties to set when initializing the Yospace session. + */ + val sessionProperties: Session.SessionProperties = Session.SessionProperties() +) : CustomSsaiDescription() { + override val customIntegrationId: String + get() = INTEGRATION_ID +} + +/** + * The type of the Yospace stream. + */ +enum class YospaceStreamType { + /** + * The stream is a live stream. + */ + LIVE, + + /** + * The stream is a live stream with a large DVR window. + */ + LIVEPAUSE, + + /** + * The stream is a Non-Linear Start-Over stream. + */ + NONLINEAR, + + /** + * The stream is a video-on-demand stream. + */ + VOD, +} diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SourceHelpers.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SourceHelpers.kt new file mode 100644 index 00000000..9e03e49f --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SourceHelpers.kt @@ -0,0 +1,28 @@ +package com.theoplayer.android.connector.yospace.internal + +import com.theoplayer.android.api.source.SourceDescription +import com.theoplayer.android.api.source.TypedSource + +internal fun SourceDescription.replaceSources(sources: List): SourceDescription { + return SourceDescription.Builder(*sources.toTypedArray()).apply { + ads(*ads.toTypedArray()) + textTracks(*textTracks.toTypedArray()) + poster?.let { poster(it) } + metadata?.let { metadata(it) } + timeServer?.let { timeServer(it) } + }.build() +} + +internal fun TypedSource.replaceSrc(src: String): TypedSource { + return TypedSource.Builder(src).apply { + drm?.let { drm(it) } + type?.let { type(it) } + liveOffset?.let { liveOffset(it) } + ssai?.let { ssai(it) } + isHlsDateRange?.let { hlsDateRange(it) } + timeServer?.let { timeServer(it) } + isLowLatency?.let { lowLatency(it) } + hls?.let { hls(it) } + dash?.let { dash(it) } + }.build() +} \ No newline at end of file diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt new file mode 100644 index 00000000..b97a09f4 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -0,0 +1,98 @@ +package com.theoplayer.android.connector.yospace.internal + +import android.util.Log +import com.theoplayer.android.api.ads.ServerSideAdIntegrationController +import com.theoplayer.android.api.ads.ServerSideAdIntegrationHandler +import com.theoplayer.android.api.source.SourceDescription +import com.theoplayer.android.connector.yospace.TAG +import com.theoplayer.android.connector.yospace.YospaceListener +import com.theoplayer.android.connector.yospace.YospaceSsaiDescription +import com.theoplayer.android.connector.yospace.YospaceStreamType +import com.yospace.admanagement.Session +import com.yospace.admanagement.SessionDVRLive +import com.yospace.admanagement.SessionLive +import com.yospace.admanagement.SessionVOD +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class YospaceAdIntegration( + private val controller: ServerSideAdIntegrationController, + private val listener: YospaceListener +) : ServerSideAdIntegrationHandler { + private var session: Session? = null + + override suspend fun setSource(source: SourceDescription): SourceDescription { + val yospaceSource = source.sources.find { it.ssai is YospaceSsaiDescription } ?: return source + val ssaiDescription = yospaceSource.ssai as? YospaceSsaiDescription ?: return source + + // Create the Yospace session + val src = yospaceSource.src + val session = when (ssaiDescription.streamType) { + YospaceStreamType.LIVE -> createSessionLive(src, ssaiDescription.sessionProperties) + + YospaceStreamType.NONLINEAR, + YospaceStreamType.LIVEPAUSE -> createSessionDVRLive(src, ssaiDescription.sessionProperties) + + YospaceStreamType.VOD -> createSessionVOD(src, ssaiDescription.sessionProperties) + } + this.session = session + + // Load the source + when (session.sessionState) { + Session.SessionState.INITIALISED, + Session.SessionState.NO_ANALYTICS -> { + // Notify listener + listener.onSessionAvailable() + // Replace source with playback URL + val newSource = source.replaceSources(source.sources.toMutableList().apply { + remove(yospaceSource) + add(0, yospaceSource.replaceSrc(session.playbackUrl)) + }) + return newSource + } + + Session.SessionState.FAILED -> { + val resultCode = session.resultCode + session.shutdown() + throw Exception(getSessionErrorMessage(resultCode)) + } + + Session.SessionState.SHUTDOWN -> { + // Already shutdown, ignore + } + + else -> { + Log.d(TAG, "Unexpected Yospace session state: ${session.sessionState}") + session.shutdown() + } + } + return source + } + + override suspend fun resetSource() { + session?.shutdown() + session = null + } +} + +private suspend fun createSessionLive(url: String, properties: Session.SessionProperties?) = suspendCoroutine { continuation -> + SessionLive.create(url, properties) { event -> continuation.resume(event.payload) } +} + +private suspend fun createSessionDVRLive(url: String, properties: Session.SessionProperties?) = suspendCoroutine { continuation -> + SessionDVRLive.create(url, properties) { event -> continuation.resume(event.payload) } +} + +private suspend fun createSessionVOD(url: String, properties: Session.SessionProperties?) = suspendCoroutine { continuation -> + SessionVOD.create(url, properties) { event -> continuation.resume(event.payload) } +} + +private fun getSessionErrorMessage(resultCode: Int): String { + val message = when (resultCode) { + YospaceSessionResultCode.MALFORMED_URL -> "The stream URL is not correctly formatted" + YospaceSessionResultCode.CONNECTION_ERROR -> "Connection error" + YospaceSessionResultCode.CONNECTION_TIMEOUT -> "Connection timeout" + else -> "Session could not be initialised" + } + return "Yospace: $message (code = $resultCode)" +} \ No newline at end of file diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceResultCode.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceResultCode.kt new file mode 100644 index 00000000..df9962b6 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceResultCode.kt @@ -0,0 +1,8 @@ +package com.theoplayer.android.connector.yospace.internal + +internal object YospaceSessionResultCode { + val CONNECTION_ERROR = -1 + val CONNECTION_TIMEOUT = -2 + val MALFORMED_URL = -3 + val UNKNOWN_FORMAT = -20 +} \ No newline at end of file From 643ab50f0c4b0e2e4051a394376c3b4591c46fa3 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 27 May 2024 16:59:02 +0200 Subject: [PATCH 04/71] Store session only when successful --- .../android/connector/yospace/internal/YospaceAdIntegration.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index b97a09f4..cb5e0f37 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -35,12 +35,11 @@ class YospaceAdIntegration( YospaceStreamType.VOD -> createSessionVOD(src, ssaiDescription.sessionProperties) } - this.session = session - // Load the source when (session.sessionState) { Session.SessionState.INITIALISED, Session.SessionState.NO_ANALYTICS -> { + this.session = session // Notify listener listener.onSessionAvailable() // Replace source with playback URL From c9d134c6f32d66e4192e32c81192a4880241ebe2 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 27 May 2024 17:00:14 +0200 Subject: [PATCH 05/71] Add AnalyticEventObserver support --- .../connector/yospace/YospaceConnector.kt | 53 +++++++++++++++++++ .../yospace/internal/YospaceAdIntegration.kt | 17 +++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt index 3aba5ba7..6c6fc35b 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt @@ -3,6 +3,11 @@ package com.theoplayer.android.connector.yospace import com.theoplayer.android.api.ads.ServerSideAdIntegrationController import com.theoplayer.android.api.player.Player import com.theoplayer.android.connector.yospace.internal.YospaceAdIntegration +import com.yospace.admanagement.AdBreak +import com.yospace.admanagement.Advert +import com.yospace.admanagement.AnalyticEventObserver +import com.yospace.admanagement.Session +import com.yospace.admanagement.TrackingErrors import java.util.concurrent.CopyOnWriteArrayList const val INTEGRATION_ID = "yospace" @@ -11,6 +16,7 @@ const val TAG = "YospaceConnector" class YospaceConnector( player: Player ) { + private val analyticEventObservers = CopyOnWriteArrayList() private val listeners = CopyOnWriteArrayList() private lateinit var integration: YospaceAdIntegration @@ -22,12 +28,21 @@ class YospaceConnector( private fun setupIntegration(controller: ServerSideAdIntegrationController): YospaceAdIntegration { val integration = YospaceAdIntegration( controller, + ForwardingAnalyticEventObserver(), ForwardingListener() ) this.integration = integration return integration } + fun registerAnalyticEventObserver(observer: AnalyticEventObserver) { + analyticEventObservers.add(observer) + } + + fun unregisterAnalyticEventObserver(observer: AnalyticEventObserver) { + analyticEventObservers.remove(observer) + } + fun addListener(listener: YospaceListener) { listeners.add(listener) } @@ -36,6 +51,44 @@ class YospaceConnector( listeners.remove(listener) } + private inner class ForwardingAnalyticEventObserver : AnalyticEventObserver { + override fun onAdvertBreakStart(adBreak: AdBreak?, session: Session) { + analyticEventObservers.forEach { it.onAdvertBreakStart(adBreak, session) } + } + + override fun onAdvertBreakEnd(session: Session) { + analyticEventObservers.forEach { it.onAdvertBreakEnd(session) } + } + + override fun onAdvertStart(advert: Advert, session: Session) { + analyticEventObservers.forEach { it.onAdvertStart(advert, session) } + } + + override fun onAdvertEnd(session: Session) { + analyticEventObservers.forEach { it.onAdvertEnd(session) } + } + + override fun onAnalyticUpdate(session: Session) { + analyticEventObservers.forEach { it.onAnalyticUpdate(session) } + } + + override fun onEarlyReturn(adBreak: AdBreak, session: Session) { + analyticEventObservers.forEach { it.onEarlyReturn(adBreak, session) } + } + + override fun onSessionError(error: AnalyticEventObserver.SessionError, session: Session) { + analyticEventObservers.forEach { it.onSessionError(error, session) } + } + + override fun onTrackingEvent(type: String, session: Session) { + analyticEventObservers.forEach { it.onTrackingEvent(type, session) } + } + + override fun onTrackingError(error: TrackingErrors.Error, session: Session) { + analyticEventObservers.forEach { it.onTrackingError(error, session) } + } + } + private inner class ForwardingListener : YospaceListener { override fun onSessionAvailable() { listeners.forEach { it.onSessionAvailable() } diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index cb5e0f37..0c778a34 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -8,6 +8,7 @@ import com.theoplayer.android.connector.yospace.TAG import com.theoplayer.android.connector.yospace.YospaceListener import com.theoplayer.android.connector.yospace.YospaceSsaiDescription import com.theoplayer.android.connector.yospace.YospaceStreamType +import com.yospace.admanagement.AnalyticEventObserver import com.yospace.admanagement.Session import com.yospace.admanagement.SessionDVRLive import com.yospace.admanagement.SessionLive @@ -17,6 +18,7 @@ import kotlin.coroutines.suspendCoroutine class YospaceAdIntegration( private val controller: ServerSideAdIntegrationController, + private val analyticEventObserver: AnalyticEventObserver, private val listener: YospaceListener ) : ServerSideAdIntegrationHandler { private var session: Session? = null @@ -39,7 +41,8 @@ class YospaceAdIntegration( when (session.sessionState) { Session.SessionState.INITIALISED, Session.SessionState.NO_ANALYTICS -> { - this.session = session + // Set up + setupSession(session) // Notify listener listener.onSessionAvailable() // Replace source with playback URL @@ -68,8 +71,18 @@ class YospaceAdIntegration( return source } + private fun setupSession(session: Session) { + this.session = session + session.apply { + addAnalyticObserver(analyticEventObserver) + } + } + override suspend fun resetSource() { - session?.shutdown() + session?.apply { + removeAnalyticObserver(analyticEventObserver) + shutdown() + } session = null } } From 41a16229be54b5e92927c6182ef0f506d4e121ab Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 27 May 2024 18:05:31 +0200 Subject: [PATCH 06/71] Serialize YospaceSsaiDescription --- build.gradle | 1 + connectors/yospace/build.gradle | 2 + .../connector/yospace/YospaceConnector.kt | 7 ++ .../yospace/YospaceSsaiDescription.kt | 103 ++++++++++++++++++ 4 files changed, 113 insertions(+) diff --git a/build.gradle b/build.gradle index 4632a94a..38a5bd4a 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ plugins { id 'com.android.library' version '8.4.1' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'org.jetbrains.dokka' version '1.9.20' + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false } tasks.register('updateVersion') { diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index f35dd9f8..812eca2a 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.serialization' } apply from: "$rootDir/connectors/publish.gradle" @@ -39,4 +40,5 @@ dependencies { prefer("3.6.7") } } + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' } \ No newline at end of file diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt index 6c6fc35b..f1c59fca 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt @@ -2,6 +2,7 @@ package com.theoplayer.android.connector.yospace import com.theoplayer.android.api.ads.ServerSideAdIntegrationController import com.theoplayer.android.api.player.Player +import com.theoplayer.android.api.source.ssai.CustomSsaiDescriptionRegistry import com.theoplayer.android.connector.yospace.internal.YospaceAdIntegration import com.yospace.admanagement.AdBreak import com.yospace.admanagement.Advert @@ -94,4 +95,10 @@ class YospaceConnector( listeners.forEach { it.onSessionAvailable() } } } + + companion object { + init { + CustomSsaiDescriptionRegistry.register(INTEGRATION_ID, YospaceSsaiDescriptionSerializer()) + } + } } diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt index dae47996..fc052a36 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt @@ -1,11 +1,21 @@ package com.theoplayer.android.connector.yospace import com.theoplayer.android.api.source.ssai.CustomSsaiDescription +import com.theoplayer.android.api.source.ssai.CustomSsaiDescriptionSerializer import com.yospace.admanagement.Session +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import java.util.UUID /** * The configuration for server-side ad insertion using the Yospace connector. */ +@Serializable(with = YospaceSsaiDescriptionKSerializer::class) class YospaceSsaiDescription( /** * The type of the requested stream. @@ -25,6 +35,7 @@ class YospaceSsaiDescription( /** * The type of the Yospace stream. */ +@Serializable enum class YospaceStreamType { /** * The stream is a live stream. @@ -46,3 +57,95 @@ enum class YospaceStreamType { */ VOD, } + +internal class YospaceSsaiDescriptionSerializer : CustomSsaiDescriptionSerializer { + override fun fromJson(json: String): CustomSsaiDescription { + return Json.decodeFromString(YospaceSsaiDescription.serializer(), json) + } + + override fun toJson(value: CustomSsaiDescription): String { + return Json.encodeToString(YospaceSsaiDescription.serializer(), value as YospaceSsaiDescription) + } +} + +@Serializable +private data class SerializedYospaceSsaiDescription( + val integration: String, + val streamType: YospaceStreamType, + val sessionProperties: SerializedSessionProperties +) + +private fun YospaceSsaiDescription.serialize() = SerializedYospaceSsaiDescription( + integration = customIntegrationId, + streamType = streamType, + sessionProperties = sessionProperties.serialize() +) + +private fun SerializedYospaceSsaiDescription.deserialize() = YospaceSsaiDescription( + streamType = streamType, + sessionProperties = sessionProperties.deserialize() +) + +@Serializable +private data class SerializedSessionProperties( + val requestTimeout: Int, + val resourceTimeout: Int, + val userAgent: String, + val proxyUserAgent: String, + val keepProxyAlive: Boolean, + val prefetchResources: Boolean, + val fireHistoricalBeacons: Boolean, + val applyEncryptedTracking: Boolean, + val excludedCategories: Int, + val consecutiveBreakTolerance: Int, + val token: String, + val customHttpHeaders: Map, +) + +private fun Session.SessionProperties.serialize() = SerializedSessionProperties( + requestTimeout = requestTimeout, + resourceTimeout = resourceTimeout, + userAgent = userAgent, + proxyUserAgent = proxyUserAgent, + keepProxyAlive = keepProxyAlive, + prefetchResources = prefetchResources, + fireHistoricalBeacons = fireHistoricalBeacons, + applyEncryptedTracking = applyEncryptedTracking, + excludedCategories = excludedCategories, + consecutiveBreakTolerance = consecutiveBreakTolerance, + token = token.toString(), + customHttpHeaders = customHttpHeaders, +) + +private fun SerializedSessionProperties.deserialize(): Session.SessionProperties { + val serialized = this + return Session.SessionProperties().apply { + requestTimeout = serialized.requestTimeout + resourceTimeout = serialized.resourceTimeout + userAgent = serialized.userAgent + proxyUserAgent = serialized.proxyUserAgent + keepProxyAlive = serialized.keepProxyAlive + prefetchResources = serialized.prefetchResources + fireHistoricalBeacons = serialized.fireHistoricalBeacons + applyEncryptedTracking = serialized.applyEncryptedTracking + excludeFromSuppression(serialized.excludedCategories) + consecutiveBreakTolerance = serialized.consecutiveBreakTolerance + token = UUID.fromString(serialized.token) + customHttpHeaders = serialized.customHttpHeaders + } +} + +private class YospaceSsaiDescriptionKSerializer : KSerializer { + private val delegateSerializer = SerializedYospaceSsaiDescription.serializer() + + @OptIn(ExperimentalSerializationApi::class) + override val descriptor = SerialDescriptor("YospaceSsaiDescription", delegateSerializer.descriptor) + + override fun serialize(encoder: Encoder, value: YospaceSsaiDescription) { + encoder.encodeSerializableValue(delegateSerializer, value.serialize()) + } + + override fun deserialize(decoder: Decoder): YospaceSsaiDescription { + return decoder.decodeSerializableValue(delegateSerializer).deserialize() + } +} From b29ec0911dbefdc8bd4c0b3d76ebacec52d3cf74 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 27 May 2024 18:46:44 +0200 Subject: [PATCH 07/71] Test serialization --- connectors/yospace/build.gradle | 7 ++ .../internal/YospaceSsaiDescriptionTests.kt | 76 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 connectors/yospace/src/test/java/com/theoplayer/android/connector/yospace/internal/YospaceSsaiDescriptionTests.kt diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index 812eca2a..0c23f057 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -12,6 +12,7 @@ android { defaultConfig { minSdk 21 consumerProguardFiles "consumer-rules.pro" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -41,4 +42,10 @@ dependencies { } } implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' + + // Tests + testImplementation "androidx.test:core:1.5.0" + testImplementation "androidx.test.ext:junit:1.1.5" + testImplementation "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion" + testImplementation "com.yospace:admanagement-sdk:3.6.7" } \ No newline at end of file diff --git a/connectors/yospace/src/test/java/com/theoplayer/android/connector/yospace/internal/YospaceSsaiDescriptionTests.kt b/connectors/yospace/src/test/java/com/theoplayer/android/connector/yospace/internal/YospaceSsaiDescriptionTests.kt new file mode 100644 index 00000000..1b00eab1 --- /dev/null +++ b/connectors/yospace/src/test/java/com/theoplayer/android/connector/yospace/internal/YospaceSsaiDescriptionTests.kt @@ -0,0 +1,76 @@ +package com.theoplayer.android.connector.yospace.internal + +import com.theoplayer.android.connector.yospace.INTEGRATION_ID +import com.theoplayer.android.connector.yospace.YospaceSsaiDescription +import com.theoplayer.android.connector.yospace.YospaceSsaiDescriptionSerializer +import com.theoplayer.android.connector.yospace.YospaceStreamType +import com.yospace.admanagement.Session.SessionProperties +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class YospaceSsaiDescriptionSerializerTests { + private lateinit var serializer: YospaceSsaiDescriptionSerializer + + @Before + fun setup() { + serializer = YospaceSsaiDescriptionSerializer() + } + + @Test + fun givenEmptySsaiDescription_WhenSerialize_ThenReturnsExpected() { + val ssaiDescription = YospaceSsaiDescription() + val jsonString = serializer.toJson(ssaiDescription) + val jsonObject = Json.parseToJsonElement(jsonString).jsonObject + assertEquals(jsonObject["integration"], JsonPrimitive(INTEGRATION_ID)) + assertEquals(jsonObject["streamType"], JsonPrimitive(YospaceStreamType.LIVE.toString())) + val jsonSessionProperties = jsonObject["sessionProperties"]?.jsonObject + assertEquals( + jsonSessionProperties?.keys, setOf( + "requestTimeout", + "resourceTimeout", + "userAgent", + "proxyUserAgent", + "keepProxyAlive", + "prefetchResources", + "fireHistoricalBeacons", + "applyEncryptedTracking", + "excludedCategories", + "consecutiveBreakTolerance", + "token", + "customHttpHeaders" + ) + ) + } + + @Test + fun givenSsaiDescriptionWithStreamType_WhenSerialize_ThenReturnsExpected() { + val ssaiDescription = YospaceSsaiDescription( + streamType = YospaceStreamType.LIVEPAUSE + ) + val jsonString = serializer.toJson(ssaiDescription) + val jsonObject = Json.parseToJsonElement(jsonString).jsonObject + assertEquals(jsonObject["streamType"], JsonPrimitive(YospaceStreamType.LIVEPAUSE.toString())) + } + + @Test + fun givenSsaiDescriptionWithSessionProperties_WhenSerialize_ThenReturnsExpected() { + val testUserAgent = "Test User Agent" + val testHeaders = mapOf("X-Hello" to "World") + val ssaiDescription = YospaceSsaiDescription( + sessionProperties = SessionProperties().apply { + userAgent = testUserAgent + customHttpHeaders = testHeaders + } + ) + val jsonString = serializer.toJson(ssaiDescription) + val jsonObject = Json.parseToJsonElement(jsonString).jsonObject + val jsonSessionProperties = jsonObject["sessionProperties"]?.jsonObject + assertEquals(jsonSessionProperties?.get("userAgent"), JsonPrimitive(testUserAgent)) + assertEquals(jsonSessionProperties?.get("customHttpHeaders"), JsonObject(testHeaders.mapValues { JsonPrimitive(it.value) })) + } +} From cd375e8b40f266f8a96a69021a5c6fa91e728695 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 27 May 2024 19:00:01 +0200 Subject: [PATCH 08/71] Simplify --- .../android/connector/yospace/YospaceSsaiDescription.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt index fc052a36..0c030b0d 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt @@ -15,7 +15,6 @@ import java.util.UUID /** * The configuration for server-side ad insertion using the Yospace connector. */ -@Serializable(with = YospaceSsaiDescriptionKSerializer::class) class YospaceSsaiDescription( /** * The type of the requested stream. @@ -59,12 +58,12 @@ enum class YospaceStreamType { } internal class YospaceSsaiDescriptionSerializer : CustomSsaiDescriptionSerializer { - override fun fromJson(json: String): CustomSsaiDescription { - return Json.decodeFromString(YospaceSsaiDescription.serializer(), json) + override fun fromJson(json: String): YospaceSsaiDescription { + return Json.decodeFromString(YospaceSsaiDescriptionKSerializer(), json) } override fun toJson(value: CustomSsaiDescription): String { - return Json.encodeToString(YospaceSsaiDescription.serializer(), value as YospaceSsaiDescription) + return Json.encodeToString(YospaceSsaiDescriptionKSerializer(), value as YospaceSsaiDescription) } } From 41b9dede175232224706e2d756b8dd3871282360 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 28 May 2024 13:30:03 +0200 Subject: [PATCH 09/71] Report player events to Yospace --- .../connector/yospace/YospaceConnector.kt | 3 +- .../yospace/internal/YospaceAdIntegration.kt | 93 ++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt index f1c59fca..f8832cab 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt @@ -15,7 +15,7 @@ const val INTEGRATION_ID = "yospace" const val TAG = "YospaceConnector" class YospaceConnector( - player: Player + val player: Player ) { private val analyticEventObservers = CopyOnWriteArrayList() private val listeners = CopyOnWriteArrayList() @@ -28,6 +28,7 @@ class YospaceConnector( private fun setupIntegration(controller: ServerSideAdIntegrationController): YospaceAdIntegration { val integration = YospaceAdIntegration( + player, controller, ForwardingAnalyticEventObserver(), ForwardingListener() diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 0c778a34..f7bb41e8 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -3,12 +3,23 @@ package com.theoplayer.android.connector.yospace.internal import android.util.Log import com.theoplayer.android.api.ads.ServerSideAdIntegrationController import com.theoplayer.android.api.ads.ServerSideAdIntegrationHandler +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.player.EndedEvent +import com.theoplayer.android.api.event.player.PauseEvent +import com.theoplayer.android.api.event.player.PlayEvent +import com.theoplayer.android.api.event.player.PlayerEventTypes +import com.theoplayer.android.api.event.player.PlayingEvent +import com.theoplayer.android.api.event.player.SeekedEvent +import com.theoplayer.android.api.event.player.VolumeChangeEvent +import com.theoplayer.android.api.event.player.WaitingEvent +import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.source.SourceDescription import com.theoplayer.android.connector.yospace.TAG import com.theoplayer.android.connector.yospace.YospaceListener import com.theoplayer.android.connector.yospace.YospaceSsaiDescription import com.theoplayer.android.connector.yospace.YospaceStreamType import com.yospace.admanagement.AnalyticEventObserver +import com.yospace.admanagement.PlaybackEventHandler import com.yospace.admanagement.Session import com.yospace.admanagement.SessionDVRLive import com.yospace.admanagement.SessionLive @@ -17,11 +28,18 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class YospaceAdIntegration( + private val player: Player, private val controller: ServerSideAdIntegrationController, private val analyticEventObserver: AnalyticEventObserver, private val listener: YospaceListener ) : ServerSideAdIntegrationHandler { private var session: Session? = null + private var didFirstPlay: Boolean = false + private var isMuted: Boolean = false + private var isStalling: Boolean = false + + private val currentPlayhead: Long + get() = (player.currentTime * 1000.0).toLong() override suspend fun setSource(source: SourceDescription): SourceDescription { val yospaceSource = source.sources.find { it.ssai is YospaceSsaiDescription } ?: return source @@ -76,15 +94,88 @@ class YospaceAdIntegration( session.apply { addAnalyticObserver(analyticEventObserver) } + addPlayerListeners() } - override suspend fun resetSource() { + private fun destroySession() { + removePlayerListeners() session?.apply { removeAnalyticObserver(analyticEventObserver) shutdown() } session = null } + + private fun addPlayerListeners() { + player.addEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange) + player.addEventListener(PlayerEventTypes.PLAY, onPlay) + player.addEventListener(PlayerEventTypes.ENDED, onEnded) + player.addEventListener(PlayerEventTypes.PAUSE, onPause) + player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.addEventListener(PlayerEventTypes.WAITING, onWaiting) + player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) + } + + private fun removePlayerListeners() { + player.removeEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange) + player.removeEventListener(PlayerEventTypes.PLAY, onPlay) + player.removeEventListener(PlayerEventTypes.ENDED, onEnded) + player.removeEventListener(PlayerEventTypes.PAUSE, onPause) + player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) + player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) + } + + private val onVolumeChange = EventListener { + val muted = player.isMuted + if (isMuted != muted) { + isMuted = muted + session?.onVolumeChange(muted) + } + } + + private val onPlay = EventListener { + if (!didFirstPlay) { + didFirstPlay = true + session?.onPlayerEvent(PlaybackEventHandler.PlayerEvent.START, currentPlayhead) + } else { + session?.onPlayerEvent(PlaybackEventHandler.PlayerEvent.RESUME, currentPlayhead) + } + } + + private val onEnded = EventListener { + session?.onPlayerEvent(PlaybackEventHandler.PlayerEvent.STOP, currentPlayhead) + } + + private val onPause = EventListener { + session?.onPlayerEvent(PlaybackEventHandler.PlayerEvent.PAUSE, currentPlayhead) + } + + private val onSeeked = EventListener { + session?.onPlayerEvent(PlaybackEventHandler.PlayerEvent.SEEK, currentPlayhead) + } + + private val onWaiting = EventListener { + isStalling = true + session?.onPlayerEvent(PlaybackEventHandler.PlayerEvent.STALL, currentPlayhead) + } + + private val onPlaying = EventListener { + if (isStalling) { + isStalling = false + session?.onPlayerEvent(PlaybackEventHandler.PlayerEvent.CONTINUE, currentPlayhead) + } + } + + override suspend fun resetSource() { + destroySession() + didFirstPlay = false + isStalling = false + } + + override suspend fun destroy() { + resetSource() + } } private suspend fun createSessionLive(url: String, properties: Session.SessionProperties?) = suspendCoroutine { continuation -> From db86f886a9b7f951a4e4e8b7a949ae919b313bfc Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 28 May 2024 17:50:38 +0200 Subject: [PATCH 10/71] Report timed metadata to Yospace --- .../yospace/internal/TimedMetadataHandler.kt | 143 ++++++++++++++++++ .../yospace/internal/YospaceAdIntegration.kt | 8 +- 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt new file mode 100644 index 00000000..085e1588 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt @@ -0,0 +1,143 @@ +package com.theoplayer.android.connector.yospace.internal + +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.track.texttrack.CueChangeEvent +import com.theoplayer.android.api.event.track.texttrack.TextTrackEventTypes +import com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent +import com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent +import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventTypes +import com.theoplayer.android.api.player.Player +import com.theoplayer.android.api.player.track.texttrack.TextTrack +import com.theoplayer.android.api.player.track.texttrack.TextTrackType +import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue +import com.yospace.admanagement.PlaybackEventHandler +import com.yospace.admanagement.TimedMetadata +import org.json.JSONArray + +private const val YOSPACE_EMSG_SCHEME_ID_URI = "urn:yospace:a:id3:2016"; + +class TimedMetadataHandler( + private val player: Player, + private val handler: PlaybackEventHandler +) { + private fun handleTrackAdded(track: TextTrack) { + if (track.kind != "metadata") { + return + } + when (track.type) { + TextTrackType.ID3 -> { + handleId3CueChange(track) + track.addEventListener(TextTrackEventTypes.CUECHANGE, onId3CueChange) + } + + TextTrackType.EMSG -> { + handleEmsgCueChange(track) + track.addEventListener(TextTrackEventTypes.CUECHANGE, onEmsgCueChange) + } + + else -> return + } + } + + private fun handleTrackRemoved(track: TextTrack) { + track.removeEventListener(TextTrackEventTypes.CUECHANGE, onId3CueChange) + track.removeEventListener(TextTrackEventTypes.CUECHANGE, onEmsgCueChange) + } + + private fun handleId3CueChange(track: TextTrack) { + val activeCues = (track.activeCues ?: return).filter(::isYospaceId3Cue) + var startTime = (activeCues.firstOrNull() ?: return).startTime + var report = YospaceReport() + for (cue in activeCues) { + // cue.content.content is an ID3 frame encoded as a JSON object. + // See `ID3Yospace` in THEOplayer Web SDK for the full type definition. + val id3 = cue.content!!.getJSONObject("content") + report.update(id3.getString("id"), id3.optString("text")) + if (cue.startTime != startTime) { + report.finish((cue.startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + report = YospaceReport() + startTime = cue.startTime + } + } + report.finish((startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + } + + private fun handleEmsgCueChange(track: TextTrack) { + val activeCues = (track.activeCues ?: return).filter(::isYospaceEmsgCue) + for (cue in activeCues) { + val report = YospaceReport() + // cue.content.content is a byte array of a UTF-8 encoded string + // holding comma-separated `key=value` pairs. + val text = jsonArrayToByteArray(cue.content!!.getJSONArray("content")).toString(Charsets.UTF_8) + for (pair in text.splitToSequence(',')) { + val (key, value) = pair.split('=', limit = 2) + report.update(key, value) + report.finish((cue.startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + } + } + } + + private val onAddTrack = EventListener { handleTrackAdded(it.track) } + private val onRemoveTrack = EventListener { handleTrackRemoved(it.track) } + private val onId3CueChange = EventListener { handleId3CueChange(it.textTrack) } + private val onEmsgCueChange = EventListener { handleEmsgCueChange(it.textTrack) } + + init { + player.textTracks.forEach { handleTrackAdded(it) } + player.textTracks.addEventListener(TextTrackListEventTypes.ADDTRACK, onAddTrack) + player.textTracks.addEventListener(TextTrackListEventTypes.REMOVETRACK, onRemoveTrack) + } + + fun destroy() { + player.textTracks.forEach { handleTrackRemoved(it) } + player.textTracks.removeEventListener(TextTrackListEventTypes.ADDTRACK, onAddTrack) + player.textTracks.removeEventListener(TextTrackListEventTypes.REMOVETRACK, onRemoveTrack) + } +} + +private data class YospaceReport( + var ymid: String? = null, + var ytyp: String? = null, + var yseq: String? = null, + var ydur: String? = null, +) { + fun update(key: String, value: String?) { + when (key.uppercase()) { + "YMID" -> ymid = value + "YTYP" -> ytyp = value + "YSEQ" -> yseq = value + "YDUR" -> ydur = value + } + } + + fun finish(playhead: Long): TimedMetadata? { + val (ymid, ytyp, yseq, ydur) = this + // Only create complete reports + return if (ymid != null && ydur != null && yseq != null && ytyp != null) { + TimedMetadata.createFromMetadata(ymid, yseq, ytyp, ydur, playhead) + } else { + null + } + } +} + +private fun isYospaceId3Cue(cue: TextTrackCue): Boolean { + val id3 = cue.content?.optJSONObject("content") ?: return false + return when (id3.optString("id")) { + "YMID", "YTYP", "YSEQ", "YDUR" -> true + else -> false + } +} + +private fun isYospaceEmsgCue(cue: TextTrackCue): Boolean { + // FIXME Check if cue.schemeIdUri == YOSPACE_EMSG_SCHEME_ID_URI + return cue.content?.optJSONArray("content") != null +} + +private fun jsonArrayToByteArray(jsonArray: JSONArray): ByteArray { + val result = ByteArray(jsonArray.length()) + for (i in 0.. Date: Wed, 29 May 2024 10:31:15 +0200 Subject: [PATCH 11/71] Move SerializedSessionProperties to separate file --- .../yospace/YospaceSsaiDescription.kt | 53 ++---------------- .../yospace/internal/SessionPropertiesUtil.kt | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 50 deletions(-) create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt index 0c030b0d..1d58a20f 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt @@ -2,6 +2,9 @@ package com.theoplayer.android.connector.yospace import com.theoplayer.android.api.source.ssai.CustomSsaiDescription import com.theoplayer.android.api.source.ssai.CustomSsaiDescriptionSerializer +import com.theoplayer.android.connector.yospace.internal.SerializedSessionProperties +import com.theoplayer.android.connector.yospace.internal.deserialize +import com.theoplayer.android.connector.yospace.internal.serialize import com.yospace.admanagement.Session import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer @@ -10,7 +13,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json -import java.util.UUID /** * The configuration for server-side ad insertion using the Yospace connector. @@ -85,55 +87,6 @@ private fun SerializedYospaceSsaiDescription.deserialize() = YospaceSsaiDescript sessionProperties = sessionProperties.deserialize() ) -@Serializable -private data class SerializedSessionProperties( - val requestTimeout: Int, - val resourceTimeout: Int, - val userAgent: String, - val proxyUserAgent: String, - val keepProxyAlive: Boolean, - val prefetchResources: Boolean, - val fireHistoricalBeacons: Boolean, - val applyEncryptedTracking: Boolean, - val excludedCategories: Int, - val consecutiveBreakTolerance: Int, - val token: String, - val customHttpHeaders: Map, -) - -private fun Session.SessionProperties.serialize() = SerializedSessionProperties( - requestTimeout = requestTimeout, - resourceTimeout = resourceTimeout, - userAgent = userAgent, - proxyUserAgent = proxyUserAgent, - keepProxyAlive = keepProxyAlive, - prefetchResources = prefetchResources, - fireHistoricalBeacons = fireHistoricalBeacons, - applyEncryptedTracking = applyEncryptedTracking, - excludedCategories = excludedCategories, - consecutiveBreakTolerance = consecutiveBreakTolerance, - token = token.toString(), - customHttpHeaders = customHttpHeaders, -) - -private fun SerializedSessionProperties.deserialize(): Session.SessionProperties { - val serialized = this - return Session.SessionProperties().apply { - requestTimeout = serialized.requestTimeout - resourceTimeout = serialized.resourceTimeout - userAgent = serialized.userAgent - proxyUserAgent = serialized.proxyUserAgent - keepProxyAlive = serialized.keepProxyAlive - prefetchResources = serialized.prefetchResources - fireHistoricalBeacons = serialized.fireHistoricalBeacons - applyEncryptedTracking = serialized.applyEncryptedTracking - excludeFromSuppression(serialized.excludedCategories) - consecutiveBreakTolerance = serialized.consecutiveBreakTolerance - token = UUID.fromString(serialized.token) - customHttpHeaders = serialized.customHttpHeaders - } -} - private class YospaceSsaiDescriptionKSerializer : KSerializer { private val delegateSerializer = SerializedYospaceSsaiDescription.serializer() diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt new file mode 100644 index 00000000..28bc9419 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt @@ -0,0 +1,54 @@ +package com.theoplayer.android.connector.yospace.internal + +import com.yospace.admanagement.Session +import kotlinx.serialization.Serializable +import java.util.UUID + +@Serializable +data class SerializedSessionProperties( + val requestTimeout: Int, + val resourceTimeout: Int, + val userAgent: String, + val proxyUserAgent: String, + val keepProxyAlive: Boolean, + val prefetchResources: Boolean, + val fireHistoricalBeacons: Boolean, + val applyEncryptedTracking: Boolean, + val excludedCategories: Int, + val consecutiveBreakTolerance: Int, + val token: String, + val customHttpHeaders: Map, +) + +fun Session.SessionProperties.serialize() = SerializedSessionProperties( + requestTimeout = requestTimeout, + resourceTimeout = resourceTimeout, + userAgent = userAgent, + proxyUserAgent = proxyUserAgent, + keepProxyAlive = keepProxyAlive, + prefetchResources = prefetchResources, + fireHistoricalBeacons = fireHistoricalBeacons, + applyEncryptedTracking = applyEncryptedTracking, + excludedCategories = excludedCategories, + consecutiveBreakTolerance = consecutiveBreakTolerance, + token = token.toString(), + customHttpHeaders = customHttpHeaders, +) + +fun SerializedSessionProperties.deserialize(): Session.SessionProperties { + val serialized = this + return Session.SessionProperties().apply { + requestTimeout = serialized.requestTimeout + resourceTimeout = serialized.resourceTimeout + userAgent = serialized.userAgent + proxyUserAgent = serialized.proxyUserAgent + keepProxyAlive = serialized.keepProxyAlive + prefetchResources = serialized.prefetchResources + fireHistoricalBeacons = serialized.fireHistoricalBeacons + applyEncryptedTracking = serialized.applyEncryptedTracking + excludeFromSuppression(serialized.excludedCategories) + consecutiveBreakTolerance = serialized.consecutiveBreakTolerance + token = UUID.fromString(serialized.token) + customHttpHeaders = serialized.customHttpHeaders + } +} \ No newline at end of file From 7509fb415a26489068da52b0fe8ece884c72abb6 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 10:33:35 +0200 Subject: [PATCH 12/71] Make internal --- .../connector/yospace/internal/SessionPropertiesUtil.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt index 28bc9419..e0aefce6 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable import java.util.UUID @Serializable -data class SerializedSessionProperties( +internal data class SerializedSessionProperties( val requestTimeout: Int, val resourceTimeout: Int, val userAgent: String, @@ -20,7 +20,7 @@ data class SerializedSessionProperties( val customHttpHeaders: Map, ) -fun Session.SessionProperties.serialize() = SerializedSessionProperties( +internal fun Session.SessionProperties.serialize() = SerializedSessionProperties( requestTimeout = requestTimeout, resourceTimeout = resourceTimeout, userAgent = userAgent, @@ -35,7 +35,7 @@ fun Session.SessionProperties.serialize() = SerializedSessionProperties( customHttpHeaders = customHttpHeaders, ) -fun SerializedSessionProperties.deserialize(): Session.SessionProperties { +internal fun SerializedSessionProperties.deserialize(): Session.SessionProperties { val serialized = this return Session.SessionProperties().apply { requestTimeout = serialized.requestTimeout From 16e8cf18b0558784a5c6c6887d13256e0a4e9a0d Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 10:45:57 +0200 Subject: [PATCH 13/71] Add SessionProperties.copy() --- .../yospace/internal/SessionPropertiesUtil.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt index e0aefce6..3b5320ae 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/SessionPropertiesUtil.kt @@ -4,6 +4,34 @@ import com.yospace.admanagement.Session import kotlinx.serialization.Serializable import java.util.UUID +internal fun Session.SessionProperties.copy( + requestTimeout: Int = this.requestTimeout, + resourceTimeout: Int = this.resourceTimeout, + userAgent: String = this.userAgent, + proxyUserAgent: String = this.proxyUserAgent, + keepProxyAlive: Boolean = this.keepProxyAlive, + prefetchResources: Boolean = this.prefetchResources, + fireHistoricalBeacons: Boolean = this.fireHistoricalBeacons, + applyEncryptedTracking: Boolean = this.applyEncryptedTracking, + excludedCategories: Int = this.excludedCategories, + consecutiveBreakTolerance: Int = this.consecutiveBreakTolerance, + token: UUID = this.token, + customHttpHeaders: Map = this.customHttpHeaders, +) = Session.SessionProperties().apply { + this.requestTimeout = requestTimeout + this.resourceTimeout = resourceTimeout + this.userAgent = userAgent + this.proxyUserAgent = proxyUserAgent + this.keepProxyAlive = keepProxyAlive + this.prefetchResources = prefetchResources + this.fireHistoricalBeacons = fireHistoricalBeacons + this.applyEncryptedTracking = applyEncryptedTracking + this.excludeFromSuppression(excludedCategories) + this.consecutiveBreakTolerance = consecutiveBreakTolerance + this.token = token + this.customHttpHeaders = customHttpHeaders +} + @Serializable internal data class SerializedSessionProperties( val requestTimeout: Int, From 947e50c972132642d8964bf62ef3b9ccda1236fb Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 10:47:05 +0200 Subject: [PATCH 14/71] Set default user agent for Yospace session --- connectors/yospace/build.gradle | 3 +++ .../android/connector/yospace/YospaceConnector.kt | 3 ++- .../connector/yospace/internal/YospaceAdIntegration.kt | 10 +++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index 0c23f057..2db5818c 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -5,6 +5,8 @@ plugins { } apply from: "$rootDir/connectors/publish.gradle" +version = sdkVersion + android { namespace 'com.theoplayer.android.connector.yospace' compileSdk 34 @@ -13,6 +15,7 @@ android { minSdk 21 consumerProguardFiles "consumer-rules.pro" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "LIBRARY_VERSION", "\"${sdkVersion}\"") } buildTypes { diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt index f8832cab..7919205f 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt @@ -12,7 +12,8 @@ import com.yospace.admanagement.TrackingErrors import java.util.concurrent.CopyOnWriteArrayList const val INTEGRATION_ID = "yospace" -const val TAG = "YospaceConnector" +internal const val TAG = "YospaceConnector" +internal const val USER_AGENT = "THEOplayerYospaceConnector/${BuildConfig.LIBRARY_VERSION}" class YospaceConnector( val player: Player diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 79f86461..1b1d812b 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -15,6 +15,7 @@ import com.theoplayer.android.api.event.player.WaitingEvent import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.source.SourceDescription import com.theoplayer.android.connector.yospace.TAG +import com.theoplayer.android.connector.yospace.USER_AGENT import com.theoplayer.android.connector.yospace.YospaceListener import com.theoplayer.android.connector.yospace.YospaceSsaiDescription import com.theoplayer.android.connector.yospace.YospaceStreamType @@ -48,13 +49,16 @@ class YospaceAdIntegration( // Create the Yospace session val src = yospaceSource.src + val sessionProperties = ssaiDescription.sessionProperties.copy( + userAgent = ssaiDescription.sessionProperties.userAgent.ifEmpty { USER_AGENT } + ) val session = when (ssaiDescription.streamType) { - YospaceStreamType.LIVE -> createSessionLive(src, ssaiDescription.sessionProperties) + YospaceStreamType.LIVE -> createSessionLive(src, sessionProperties) YospaceStreamType.NONLINEAR, - YospaceStreamType.LIVEPAUSE -> createSessionDVRLive(src, ssaiDescription.sessionProperties) + YospaceStreamType.LIVEPAUSE -> createSessionDVRLive(src, sessionProperties) - YospaceStreamType.VOD -> createSessionVOD(src, ssaiDescription.sessionProperties) + YospaceStreamType.VOD -> createSessionVOD(src, sessionProperties) } when (session.sessionState) { From 9a819d2223bd332651fc1bd7522e866f978f948d Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 10:50:18 +0200 Subject: [PATCH 15/71] Use lateinit for connectors in sample app --- .../java/com/theoplayer/android/connector/MainActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 97b8a1af..d6d93173 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -24,8 +24,8 @@ import com.theoplayer.android.connector.analytics.comscore.ComscoreMetaData class MainActivity : AppCompatActivity() { private lateinit var theoplayerView: THEOplayerView - private var convivaConnector: ConvivaConnector? = null - private var nielsenConnector: NielsenConnector? = null + private lateinit var convivaConnector: ConvivaConnector + private lateinit var nielsenConnector: NielsenConnector private lateinit var comscoreConnector: ComscoreConnector override fun onCreate(savedInstanceState: Bundle?) { @@ -141,7 +141,7 @@ class MainActivity : AppCompatActivity() { .build() ).metadata(MetadataDescription(mapOf("title" to "BigBuckBunny with Google IMA ads"))) .build() - nielsenConnector?.updateMetadata(hashMapOf( + nielsenConnector.updateMetadata(hashMapOf( "assetid" to "C112233", "program" to "BigBuckBunny with Google IMA ads" )) From 52be66d7ee962f7811625b889af34c15e295238b Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 10:50:57 +0200 Subject: [PATCH 16/71] Reformat --- .../android/connector/MainActivity.kt | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) 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 d6d93173..1ccf17f5 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -13,13 +13,13 @@ import com.theoplayer.android.api.source.SourceDescription import com.theoplayer.android.api.source.TypedSource import com.theoplayer.android.api.source.addescription.GoogleImaAdDescription import com.theoplayer.android.api.source.metadata.MetadataDescription -import com.theoplayer.android.connector.analytics.conviva.ConvivaConfiguration -import com.theoplayer.android.connector.analytics.conviva.ConvivaConnector -import com.theoplayer.android.connector.analytics.nielsen.NielsenConnector import com.theoplayer.android.connector.analytics.comscore.ComscoreConfiguration import com.theoplayer.android.connector.analytics.comscore.ComscoreConnector 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.nielsen.NielsenConnector class MainActivity : AppCompatActivity() { @@ -131,20 +131,24 @@ class MainActivity : AppCompatActivity() { } fun setSource(view: View) { - theoplayerView.player.source = SourceDescription.Builder( - TypedSource.Builder("https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8") - .build() + theoplayerView.player.source = SourceDescription + .Builder( + TypedSource.Builder("https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8") + .build() + ) + .ads( + GoogleImaAdDescription.Builder("https://cdn.theoplayer.com/demos/ads/vast/dfp-linear-inline-no-skip.xml") + .timeOffset("5") + .build() + ) + .metadata(MetadataDescription(mapOf("title" to "BigBuckBunny with Google IMA ads"))) + .build() + nielsenConnector.updateMetadata( + hashMapOf( + "assetid" to "C112233", + "program" to "BigBuckBunny with Google IMA ads" + ) ) - .ads( - GoogleImaAdDescription.Builder("https://cdn.theoplayer.com/demos/ads/vast/dfp-linear-inline-no-skip.xml") - .timeOffset("5") - .build() - ).metadata(MetadataDescription(mapOf("title" to "BigBuckBunny with Google IMA ads"))) - .build() - nielsenConnector.updateMetadata(hashMapOf( - "assetid" to "C112233", - "program" to "BigBuckBunny with Google IMA ads" - )) } fun playPause(view: View) { From 2c1d9492026a573fba873d94408194d34930e24c Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 10:53:07 +0200 Subject: [PATCH 17/71] Add yospaceVersion to gradle.properties --- connectors/yospace/build.gradle | 4 ++-- gradle.properties | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index 2db5818c..577123e5 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -41,7 +41,7 @@ dependencies { compileOnly("com.yospace:admanagement-sdk") { version { strictly("[3.6, 4.0)") - prefer("3.6.7") + prefer(yospaceVersion) } } implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' @@ -50,5 +50,5 @@ dependencies { testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion" - testImplementation "com.yospace:admanagement-sdk:3.6.7" + testImplementation "com.yospace:admanagement-sdk:$yospaceVersion" } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 6129ca74..cf0c124e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,5 +28,6 @@ sdkVersion=7.6.0 lifecycleVersion=2.5.1 convivaVersion=4.0.35 comscoreVersion=6.10.0 +yospaceVersion=3.6.7 android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false \ No newline at end of file From 9402eaa113830d1584bef8b96749348a0b10b323 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 11:14:38 +0200 Subject: [PATCH 18/71] Update YospaceSsaiDescription to latest API --- .../android/connector/yospace/YospaceSsaiDescription.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt index 1d58a20f..7a54bca6 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt @@ -29,7 +29,7 @@ class YospaceSsaiDescription( */ val sessionProperties: Session.SessionProperties = Session.SessionProperties() ) : CustomSsaiDescription() { - override val customIntegrationId: String + override val customIntegration: String get() = INTEGRATION_ID } @@ -77,7 +77,7 @@ private data class SerializedYospaceSsaiDescription( ) private fun YospaceSsaiDescription.serialize() = SerializedYospaceSsaiDescription( - integration = customIntegrationId, + integration = customIntegration, streamType = streamType, sessionProperties = sessionProperties.serialize() ) From ebeb131763624d36b86f0d08e561c416d538c4f2 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 11:16:50 +0200 Subject: [PATCH 19/71] Add source select dialog to sample app --- .../android/connector/MainActivity.kt | 42 +++++++++---------- .../theoplayer/android/connector/Sources.kt | 36 ++++++++++++++++ app/src/main/res/layout/activity_main.xml | 2 +- 3 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/theoplayer/android/connector/Sources.kt 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 1ccf17f5..3bf1e4b8 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -5,14 +5,11 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.theoplayer.android.api.THEOplayerConfig import com.theoplayer.android.api.THEOplayerView import com.theoplayer.android.api.ads.ima.GoogleImaIntegrationFactory -import com.theoplayer.android.api.source.SourceDescription -import com.theoplayer.android.api.source.TypedSource -import com.theoplayer.android.api.source.addescription.GoogleImaAdDescription -import com.theoplayer.android.api.source.metadata.MetadataDescription import com.theoplayer.android.connector.analytics.comscore.ComscoreConfiguration import com.theoplayer.android.connector.analytics.comscore.ComscoreConnector import com.theoplayer.android.connector.analytics.comscore.ComscoreMediaType @@ -27,6 +24,7 @@ class MainActivity : AppCompatActivity() { private lateinit var convivaConnector: ConvivaConnector private lateinit var nielsenConnector: NielsenConnector private lateinit var comscoreConnector: ComscoreConnector + private var selectedSource: Source = sources.first() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -130,25 +128,23 @@ class MainActivity : AppCompatActivity() { nielsenConnector = NielsenConnector(applicationContext, theoplayerView.player, appId, true) } - fun setSource(view: View) { - theoplayerView.player.source = SourceDescription - .Builder( - TypedSource.Builder("https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8") - .build() - ) - .ads( - GoogleImaAdDescription.Builder("https://cdn.theoplayer.com/demos/ads/vast/dfp-linear-inline-no-skip.xml") - .timeOffset("5") - .build() - ) - .metadata(MetadataDescription(mapOf("title" to "BigBuckBunny with Google IMA ads"))) - .build() - nielsenConnector.updateMetadata( - hashMapOf( - "assetid" to "C112233", - "program" to "BigBuckBunny with Google IMA ads" - ) - ) + fun selectSource(view: View) { + val sourceNames = sources.map { it.name }.toTypedArray() + val selectedIndex = sources.indexOf(selectedSource) + AlertDialog.Builder(this) + .setTitle("Select source") + .setSingleChoiceItems(sourceNames, selectedIndex) { dialog, which -> + setSource(sources[which]) + dialog.dismiss() + } + .create() + .show() + } + + private fun setSource(source: Source) { + selectedSource = source + theoplayerView.player.source = source.sourceDescription + nielsenConnector.updateMetadata(source.nielsenMetadata) } fun playPause(view: View) { diff --git a/app/src/main/java/com/theoplayer/android/connector/Sources.kt b/app/src/main/java/com/theoplayer/android/connector/Sources.kt new file mode 100644 index 00000000..c59377ab --- /dev/null +++ b/app/src/main/java/com/theoplayer/android/connector/Sources.kt @@ -0,0 +1,36 @@ +package com.theoplayer.android.connector + +import com.theoplayer.android.api.source.SourceDescription +import com.theoplayer.android.api.source.TypedSource +import com.theoplayer.android.api.source.addescription.GoogleImaAdDescription +import com.theoplayer.android.api.source.metadata.MetadataDescription + +data class Source( + val name: String, + val sourceDescription: SourceDescription, + val nielsenMetadata: HashMap = hashMapOf() +) + +val sources: List by lazy { + listOf( + Source( + name = "BigBuckBunny with Google IMA ads", + sourceDescription = SourceDescription + .Builder( + TypedSource.Builder("https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8") + .build() + ) + .ads( + GoogleImaAdDescription.Builder("https://cdn.theoplayer.com/demos/ads/vast/dfp-linear-inline-no-skip.xml") + .timeOffset("5") + .build() + ) + .metadata(MetadataDescription(mapOf("title" to "BigBuckBunny with Google IMA ads"))) + .build(), + nielsenMetadata = hashMapOf( + "assetid" to "C112233", + "program" to "BigBuckBunny with Google IMA ads" + ) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0071c604..8ba4d19a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -27,7 +27,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="set source" - android:onClick="setSource"/> + android:onClick="selectSource"/> Date: Wed, 29 May 2024 11:19:10 +0200 Subject: [PATCH 20/71] Improve type in NielsenHandler --- .../analytics/nielsen/NielsenHandler.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/connectors/analytics/nielsen/src/main/java/com/theoplayer/android/connector/analytics/nielsen/NielsenHandler.kt b/connectors/analytics/nielsen/src/main/java/com/theoplayer/android/connector/analytics/nielsen/NielsenHandler.kt index 88d9498c..61edbb5e 100644 --- a/connectors/analytics/nielsen/src/main/java/com/theoplayer/android/connector/analytics/nielsen/NielsenHandler.kt +++ b/connectors/analytics/nielsen/src/main/java/com/theoplayer/android/connector/analytics/nielsen/NielsenHandler.kt @@ -63,7 +63,7 @@ class NielsenHandler( private val onCueEnterEmsg: EventListener private var lastPosition: Long = -1 - private var appSdk: AppSdk? = null + private var appSdk: AppSdk private var sessionInProgress: Boolean = false private val mainHandler = Handler(Looper.getMainLooper()) @@ -83,7 +83,7 @@ class NielsenHandler( maybeSendPlayEvent() } onPause = EventListener { - appSdk?.stop() + appSdk.stop() } onDurationChange = EventListener { maybeSendPlayEvent() @@ -96,19 +96,19 @@ class NielsenHandler( } onLoadedMetadata = EventListener { // contentMetadataObject contains the JSON metadata for the content being played - appSdk?.loadMetadata(buildMetadata()) + appSdk.loadMetadata(buildMetadata()) } onCueEnterId3 = EventListener { event -> event.cue.content?.optJSONObject("content")?.let { cueContent -> handleNielsenId3Payload(cueContent) { - appSdk?.sendID3(it) + appSdk.sendID3(it) } } } onCueEnterEmsg = EventListener { event -> event.cue.content?.optJSONArray("content")?.let { cueContent -> handleNielsenEmsgPayload(cueContent) { - appSdk?.sendID3(it) + appSdk.sendID3(it) } } } @@ -128,8 +128,8 @@ class NielsenHandler( val ad = event.ad if (ad?.type == "linear") { val timeOffset = ad.adBreak?.timeOffset ?: 0 - appSdk?.stop() - appSdk?.loadMetadata(buildMetadata().apply { + appSdk.stop() + appSdk.loadMetadata(buildMetadata().apply { put( PROP_TYPE, when { timeOffset == 0 -> PROP_PREROLL @@ -144,7 +144,7 @@ class NielsenHandler( onAdEnd = EventListener { event -> val ad = event.ad if (ad?.type == "linear") { - appSdk?.stop() + appSdk.stop() } } @@ -170,7 +170,7 @@ class NielsenHandler( } fun updateMetadata(metadata: Map) { - appSdk?.loadMetadata(buildMetadata(metadata)) + appSdk.loadMetadata(buildMetadata(metadata)) } fun destroy() { @@ -213,7 +213,7 @@ class NielsenHandler( sessionInProgress = true // stream starts - appSdk?.play(JSONObject().apply { + appSdk.play(JSONObject().apply { put(PROP_CHANNEL_NAME, player.src) }) } @@ -223,7 +223,7 @@ class NielsenHandler( if (sessionInProgress) { lastPosition = -1 sessionInProgress = false - appSdk?.end() + appSdk.end() } } From 9024825dd88f262b301655f09e42212acb867d14 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 11:28:46 +0200 Subject: [PATCH 21/71] Add YospaceSsaiDescription.Builder --- .../yospace/YospaceSsaiDescription.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt index 7a54bca6..a2e0bd1a 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt @@ -31,6 +31,29 @@ class YospaceSsaiDescription( ) : CustomSsaiDescription() { override val customIntegration: String get() = INTEGRATION_ID + + /** + * A builder for a [YospaceSsaiDescription]. + */ + class Builder() { + private var streamType: YospaceStreamType = YospaceStreamType.LIVE + private var sessionProperties: Session.SessionProperties = Session.SessionProperties() + + /** + * Sets the type of the requested stream. + */ + fun streamType(streamType: YospaceStreamType) = apply { this.streamType = streamType } + + /** + * Sets the custom properties to set when initializing the Yospace session. + */ + fun sessionProperties(sessionProperties: Session.SessionProperties) = apply { this.sessionProperties = sessionProperties } + + /** + * Builds the [YospaceSsaiDescription]. + */ + fun build() = YospaceSsaiDescription(streamType = streamType, sessionProperties = sessionProperties) + } } /** From 68f6790cd050a75064ac7416297f1427a0492f3e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 11:29:08 +0200 Subject: [PATCH 22/71] Add Yospace connector to sample app --- app/build.gradle | 2 ++ .../java/com/theoplayer/android/connector/MainActivity.kt | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 1260da3f..eab69542 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,4 +47,6 @@ dependencies { implementation "com.nielsenappsdk.global:ad:9.2.0.0" implementation project(':connectors:analytics:comscore') implementation "com.comscore:android-analytics:$comscoreVersion" + implementation project(':connectors:yospace') + implementation "com.yospace:admanagement-sdk:$yospaceVersion" } \ No newline at end of file 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 3bf1e4b8..9d340b22 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -17,6 +17,7 @@ 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.nielsen.NielsenConnector +import com.theoplayer.android.connector.yospace.YospaceConnector class MainActivity : AppCompatActivity() { @@ -24,6 +25,7 @@ class MainActivity : AppCompatActivity() { private lateinit var convivaConnector: ConvivaConnector private lateinit var nielsenConnector: NielsenConnector private lateinit var comscoreConnector: ComscoreConnector + private lateinit var yospaceConnector: YospaceConnector private var selectedSource: Source = sources.first() override fun onCreate(savedInstanceState: Bundle?) { @@ -35,6 +37,7 @@ class MainActivity : AppCompatActivity() { setupConviva() setupComscore() setupNielsen() + setupYospace() } private fun setupComscore() { @@ -128,6 +131,10 @@ class MainActivity : AppCompatActivity() { nielsenConnector = NielsenConnector(applicationContext, theoplayerView.player, appId, true) } + private fun setupYospace() { + yospaceConnector = YospaceConnector(theoplayerView.player) + } + fun selectSource(view: View) { val sourceNames = sources.map { it.name }.toTypedArray() val selectedIndex = sources.indexOf(selectedSource) From 0f1311ea56c707824e8eaefb9ab0850196a1735a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 11:48:43 +0200 Subject: [PATCH 23/71] Add Yospace test sources to sample app --- .../theoplayer/android/connector/Sources.kt | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/app/src/main/java/com/theoplayer/android/connector/Sources.kt b/app/src/main/java/com/theoplayer/android/connector/Sources.kt index c59377ab..bba4a929 100644 --- a/app/src/main/java/com/theoplayer/android/connector/Sources.kt +++ b/app/src/main/java/com/theoplayer/android/connector/Sources.kt @@ -1,9 +1,12 @@ package com.theoplayer.android.connector import com.theoplayer.android.api.source.SourceDescription +import com.theoplayer.android.api.source.SourceType import com.theoplayer.android.api.source.TypedSource import com.theoplayer.android.api.source.addescription.GoogleImaAdDescription import com.theoplayer.android.api.source.metadata.MetadataDescription +import com.theoplayer.android.connector.yospace.YospaceSsaiDescription +import com.theoplayer.android.connector.yospace.YospaceStreamType data class Source( val name: String, @@ -31,6 +34,71 @@ val sources: List by lazy { "assetid" to "C112233", "program" to "BigBuckBunny with Google IMA ads" ) + ), + Source( + name = "Yospace HLS VOD", + sourceDescription = SourceDescription + .Builder( + TypedSource.Builder( + "https://csm-e-sdk-validation.bln1.yospace.com/csm/access/156611618/c2FtcGxlL21hc3Rlci5tM3U4?yo.av=4" + ) + .type(SourceType.HLS) + .ssai(YospaceSsaiDescription(streamType = YospaceStreamType.VOD)) + .build() + ) + .build() + ), + Source( + name = "Yospace HLS Live", + sourceDescription = SourceDescription + .Builder( + TypedSource.Builder( + "https://csm-e-sdk-validation.bln1.yospace.com/csm/extlive/yospace02,hlssample42.m3u8?yo.br=true&yo.av=4" + ) + .type(SourceType.HLS) + .ssai(YospaceSsaiDescription(streamType = YospaceStreamType.LIVE)) + .build() + ) + .build() + ), + Source( + name = "Yospace HLS DVRLive", + sourceDescription = SourceDescription + .Builder( + TypedSource.Builder( + "https://csm-e-sdk-validation.bln1.yospace.com/csm/extlive/yospace02,hlssample42.m3u8?yo.br=true&yo.lp=true&yo.av=4" + ) + .type(SourceType.HLS) + .ssai(YospaceSsaiDescription(streamType = YospaceStreamType.LIVEPAUSE)) + .build() + ) + .build() + ), + Source( + name = "Yospace DASH Live", + sourceDescription = SourceDescription + .Builder( + TypedSource.Builder( + "https://csm-e-sdk-validation.bln1.yospace.com/csm/extlive/yosdk01,t2-dash.mpd?yo.br=true&yo.av=4" + ) + .type(SourceType.DASH) + .ssai(YospaceSsaiDescription(streamType = YospaceStreamType.LIVE)) + .build() + ) + .build() + ), + Source( + name = "Yospace DASH DVRLive", + sourceDescription = SourceDescription + .Builder( + TypedSource.Builder( + "https://csm-e-sdk-validation.bln1.yospace.com/csm/extlive/yosdk01,dash.mpd?yo.br=true&yo.lp=true&yo.jt=1000&yo.av=4" + ) + .type(SourceType.DASH) + .ssai(YospaceSsaiDescription(streamType = YospaceStreamType.LIVEPAUSE)) + .build() + ) + .build() ) ) } \ No newline at end of file From 6b2f62a02deeb118f3d2aef4bc8bc4dea165f2c2 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 11:55:36 +0200 Subject: [PATCH 24/71] Extract finishReport helper --- .../connector/yospace/internal/TimedMetadataHandler.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt index 085e1588..39abd64c 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt @@ -54,12 +54,12 @@ class TimedMetadataHandler( val id3 = cue.content!!.getJSONObject("content") report.update(id3.getString("id"), id3.optString("text")) if (cue.startTime != startTime) { - report.finish((cue.startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + finishReport(report, cue.startTime) report = YospaceReport() startTime = cue.startTime } } - report.finish((startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + finishReport(report, startTime) } private fun handleEmsgCueChange(track: TextTrack) { @@ -72,11 +72,15 @@ class TimedMetadataHandler( for (pair in text.splitToSequence(',')) { val (key, value) = pair.split('=', limit = 2) report.update(key, value) - report.finish((cue.startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + finishReport(report, cue.startTime) } } } + private fun finishReport(report: YospaceReport, startTime: Double) { + report.finish((startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + } + private val onAddTrack = EventListener { handleTrackAdded(it.track) } private val onRemoveTrack = EventListener { handleTrackRemoved(it.track) } private val onId3CueChange = EventListener { handleId3CueChange(it.textTrack) } From 20a28927dc81b1f1072444ab9ebaeb2c223a1f61 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 11:57:59 +0200 Subject: [PATCH 25/71] Fix incorrect metadata --- .../connector/yospace/internal/TimedMetadataHandler.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt index 39abd64c..5097e405 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt @@ -49,15 +49,15 @@ class TimedMetadataHandler( var startTime = (activeCues.firstOrNull() ?: return).startTime var report = YospaceReport() for (cue in activeCues) { + if (cue.startTime != startTime) { + finishReport(report, startTime) + report = YospaceReport() + } // cue.content.content is an ID3 frame encoded as a JSON object. // See `ID3Yospace` in THEOplayer Web SDK for the full type definition. val id3 = cue.content!!.getJSONObject("content") report.update(id3.getString("id"), id3.optString("text")) - if (cue.startTime != startTime) { - finishReport(report, cue.startTime) - report = YospaceReport() - startTime = cue.startTime - } + startTime = cue.startTime } finishReport(report, startTime) } From c6e777120f66c918a22e57d948af0c7275ce7d4e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 12:25:25 +0200 Subject: [PATCH 26/71] Report playhead updates --- .../yospace/internal/YospaceAdIntegration.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 1b1d812b..1c55782f 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -10,6 +10,7 @@ import com.theoplayer.android.api.event.player.PlayEvent import com.theoplayer.android.api.event.player.PlayerEventTypes import com.theoplayer.android.api.event.player.PlayingEvent import com.theoplayer.android.api.event.player.SeekedEvent +import com.theoplayer.android.api.event.player.TimeUpdateEvent import com.theoplayer.android.api.event.player.VolumeChangeEvent import com.theoplayer.android.api.event.player.WaitingEvent import com.theoplayer.android.api.player.Player @@ -100,6 +101,7 @@ class YospaceAdIntegration( addAnalyticObserver(analyticEventObserver) } addPlayerListeners() + updatePlayhead() timedMetadataHandler = TimedMetadataHandler(player, session) } @@ -124,6 +126,7 @@ class YospaceAdIntegration( player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) player.addEventListener(PlayerEventTypes.WAITING, onWaiting) player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) + player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) } private fun removePlayerListeners() { @@ -134,6 +137,7 @@ class YospaceAdIntegration( player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) + player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) } private val onVolumeChange = EventListener { @@ -177,6 +181,14 @@ class YospaceAdIntegration( } } + private val onTimeUpdate = EventListener { + updatePlayhead() + } + + private fun updatePlayhead() { + session?.onPlayheadUpdate(currentPlayhead) + } + override suspend fun resetSource() { destroySession() didFirstPlay = false From 0dd1734d1f09cc9411873c8eea07531830b876a5 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 13:18:38 +0200 Subject: [PATCH 27/71] Fix currentPlayhead for DVRLive sessions --- .../yospace/internal/YospaceAdIntegration.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 1c55782f..5de432a6 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -28,6 +28,7 @@ import com.yospace.admanagement.SessionLive import com.yospace.admanagement.SessionVOD import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import kotlin.math.max class YospaceAdIntegration( private val player: Player, @@ -42,7 +43,17 @@ class YospaceAdIntegration( private var isStalling: Boolean = false private val currentPlayhead: Long - get() = (player.currentTime * 1000.0).toLong() + get() { + var playhead = (player.currentTime * 1000.0).toLong() + + // For Yospace DVRLive sessions we need to offset the playback position from the stream start time + val session = this.session + if (session is SessionDVRLive) { + playhead += session.windowStart - max(session.streamStart, 0) + } + + return playhead + } override suspend fun setSource(source: SourceDescription): SourceDescription { val yospaceSource = source.sources.find { it.ssai is YospaceSsaiDescription } ?: return source From 430d5995be593a9d47ac4dcc99046cd3b28fcfcd Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 14:20:39 +0200 Subject: [PATCH 28/71] Add Yospace ads and ad breaks to THEOplayer --- .../connector/yospace/internal/AdHandler.kt | 125 ++++++++++++++++++ .../yospace/internal/YospaceAdIntegration.kt | 17 ++- 2 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt new file mode 100644 index 00000000..67122ba9 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt @@ -0,0 +1,125 @@ +package com.theoplayer.android.connector.yospace.internal + +import com.theoplayer.android.api.ads.Ad +import com.theoplayer.android.api.ads.AdBreak +import com.theoplayer.android.api.ads.AdBreakInit +import com.theoplayer.android.api.ads.AdInit +import com.theoplayer.android.api.ads.ServerSideAdIntegrationController +import com.yospace.admanagement.AnalyticEventObserver +import com.yospace.admanagement.Resource +import com.yospace.admanagement.Session +import com.yospace.admanagement.TrackingErrors +import java.util.WeakHashMap +import kotlin.math.max +import com.yospace.admanagement.AdBreak as YospaceAdBreak +import com.yospace.admanagement.Advert as YospaceAdvert + +class AdHandler(private val controller: ServerSideAdIntegrationController) : AnalyticEventObserver { + private val ads: WeakHashMap = WeakHashMap() + private val adBreaks: WeakHashMap = WeakHashMap() + private var currentAd: Ad? = null + private var currentYospaceAdvert: YospaceAdvert? = null + private var currentAdBreak: AdBreak? = null + + private fun getOrCreateAdBreak(yospaceAdBreak: YospaceAdBreak): AdBreak { + val adBreak = adBreaks.getOrPut(yospaceAdBreak) { controller.createAdBreak(getAdBreakInit(yospaceAdBreak)) } + yospaceAdBreak.adverts.forEach { getOrCreateAd(it, adBreak) } + return adBreak + } + + private fun getAdBreakInit(yospaceAdBreak: YospaceAdBreak): AdBreakInit { + return AdBreakInit(timeOffset = (yospaceAdBreak.start / 1000).toInt(), maxDuration = (yospaceAdBreak.duration / 1000)) + } + + private fun getOrCreateAd(yospaceAdvert: YospaceAdvert, adBreak: AdBreak): Ad { + var update = true + val ad = ads.getOrPut(yospaceAdvert) { + update = false + controller.createAd(getAdInit(yospaceAdvert), adBreak) + } + if (update) { + controller.updateAd(ad, getAdInit(yospaceAdvert)) + } + return ad + } + + private fun getAdInit(advert: YospaceAdvert): AdInit { + val nonLinearCreative = if (advert.isNonLinear) advert.getNonLinearCreatives(Resource.ResourceType.STATIC).firstOrNull() else null + return AdInit( + type = if (advert.isNonLinear) "nonlinear" else "linear", + timeOffset = (advert.start / 1000).toInt(), + skipOffset = (advert.skipOffset / 1000).toInt(), + id = advert.identifier, + duration = (advert.duration / 1000).toInt(), + clickThrough = if (advert.isNonLinear) nonLinearCreative?.clickThroughUrl else advert.linearCreative?.clickThroughUrl, + resourceURI = nonLinearCreative?.getResource(Resource.ResourceType.STATIC)?.stringData + ) + } + + override fun onAdvertBreakStart(adBreak: YospaceAdBreak?, session: Session) { + currentAdBreak = getOrCreateAdBreak(adBreak ?: return) + } + + override fun onAdvertBreakEnd(session: Session) { + currentAdBreak?.let { + controller.removeAdBreak(it) + currentAdBreak = null + } + } + + override fun onAdvertStart(advert: YospaceAdvert, session: Session) { + val ad = getOrCreateAd(advert, currentAdBreak ?: return) + currentAd = ad + currentYospaceAdvert = advert + controller.beginAd(ad) + + advert.linearCreative?.let { + // val clickThroughUrl = it.clickThroughUrl + // TODO Show linear clickthrough button + } + + advert.getNonLinearCreatives(Resource.ResourceType.STATIC).forEach { + // val clickThroughUrl = it.clickThroughUrl + // TODO Show nonlinear clickthrough button + } + } + + override fun onAdvertEnd(session: Session) { + currentAd?.let { + controller.endAd(it) + currentAd = null + currentYospaceAdvert = null + } + // TODO Remove clickthrough buttons + } + + override fun onEarlyReturn(adBreak: YospaceAdBreak, session: Session) { + currentAd?.let { + controller.skipAd(it) + currentAd = null + currentYospaceAdvert = null + } + onAdvertBreakEnd(session) + } + + override fun onAnalyticUpdate(session: Session) {} + + override fun onSessionError(error: AnalyticEventObserver.SessionError, session: Session) {} + + override fun onTrackingEvent(type: String, session: Session) {} + + override fun onTrackingError(error: TrackingErrors.Error, session: Session) {} + + fun onTimeUpdate(playhead: Long) { + val ad = this.currentAd ?: return + val advert = this.currentYospaceAdvert ?: return + val duration = advert.duration + val remainingTime = advert.getRemainingTime(playhead) + val progress = max(0.0, (duration - remainingTime).toDouble() / duration.toDouble()) + controller.updateAdProgress(ad, progress) + } + + fun destroy() { + controller.removeAllAds() + } +} \ No newline at end of file diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 5de432a6..2c3688d1 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -38,6 +38,7 @@ class YospaceAdIntegration( ) : ServerSideAdIntegrationHandler { private var session: Session? = null private var timedMetadataHandler: TimedMetadataHandler? = null + private var adHandler: AdHandler? = null private var didFirstPlay: Boolean = false private var isMuted: Boolean = false private var isStalling: Boolean = false @@ -108,12 +109,13 @@ class YospaceAdIntegration( private fun setupSession(session: Session) { this.session = session - session.apply { - addAnalyticObserver(analyticEventObserver) - } addPlayerListeners() updatePlayhead() timedMetadataHandler = TimedMetadataHandler(player, session) + adHandler = AdHandler(controller).also { + session.addAnalyticObserver(it) + } + session.addAnalyticObserver(analyticEventObserver) } private fun destroySession() { @@ -122,6 +124,11 @@ class YospaceAdIntegration( destroy() timedMetadataHandler = null } + adHandler?.apply { + session?.removeAnalyticObserver(this) + destroy() + adHandler = null + } session?.apply { removeAnalyticObserver(analyticEventObserver) shutdown() @@ -197,7 +204,9 @@ class YospaceAdIntegration( } private fun updatePlayhead() { - session?.onPlayheadUpdate(currentPlayhead) + val playhead = currentPlayhead + session?.onPlayheadUpdate(playhead) + adHandler?.onTimeUpdate(playhead) } override suspend fun resetSource() { From e18960051ba3946e24141ed7174dc3d67888acaa Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 14:21:12 +0200 Subject: [PATCH 29/71] Tweaks --- connectors/yospace/build.gradle | 2 +- .../connector/yospace/internal/YospaceAdIntegration.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index 577123e5..15662774 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -44,7 +44,7 @@ dependencies { prefer(yospaceVersion) } } - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" // Tests testImplementation "androidx.test:core:1.5.0" diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 2c3688d1..1aac649d 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -109,13 +109,13 @@ class YospaceAdIntegration( private fun setupSession(session: Session) { this.session = session - addPlayerListeners() - updatePlayhead() timedMetadataHandler = TimedMetadataHandler(player, session) adHandler = AdHandler(controller).also { session.addAnalyticObserver(it) } session.addAnalyticObserver(analyticEventObserver) + addPlayerListeners() + updatePlayhead() } private fun destroySession() { From e00c3a15164bb0e2ed425641380d6c8591e04b6a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 14:48:57 +0200 Subject: [PATCH 30/71] Refactor --- .../yospace/internal/TimedMetadataHandler.kt | 33 ++++++++++++------- .../yospace/internal/YospaceAdIntegration.kt | 8 ++++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt index 5097e405..8b661aaa 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/TimedMetadataHandler.kt @@ -10,15 +10,13 @@ import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackType import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue -import com.yospace.admanagement.PlaybackEventHandler -import com.yospace.admanagement.TimedMetadata import org.json.JSONArray private const val YOSPACE_EMSG_SCHEME_ID_URI = "urn:yospace:a:id3:2016"; -class TimedMetadataHandler( +internal class TimedMetadataHandler( private val player: Player, - private val handler: PlaybackEventHandler + private val callback: TimedMetadataCallback ) { private fun handleTrackAdded(track: TextTrack) { if (track.kind != "metadata") { @@ -47,11 +45,11 @@ class TimedMetadataHandler( private fun handleId3CueChange(track: TextTrack) { val activeCues = (track.activeCues ?: return).filter(::isYospaceId3Cue) var startTime = (activeCues.firstOrNull() ?: return).startTime - var report = YospaceReport() + var report = PendingTimedMetadata() for (cue in activeCues) { if (cue.startTime != startTime) { finishReport(report, startTime) - report = YospaceReport() + report = PendingTimedMetadata() } // cue.content.content is an ID3 frame encoded as a JSON object. // See `ID3Yospace` in THEOplayer Web SDK for the full type definition. @@ -65,7 +63,7 @@ class TimedMetadataHandler( private fun handleEmsgCueChange(track: TextTrack) { val activeCues = (track.activeCues ?: return).filter(::isYospaceEmsgCue) for (cue in activeCues) { - val report = YospaceReport() + val report = PendingTimedMetadata() // cue.content.content is a byte array of a UTF-8 encoded string // holding comma-separated `key=value` pairs. val text = jsonArrayToByteArray(cue.content!!.getJSONArray("content")).toString(Charsets.UTF_8) @@ -77,8 +75,8 @@ class TimedMetadataHandler( } } - private fun finishReport(report: YospaceReport, startTime: Double) { - report.finish((startTime * 1000).toLong())?.let { handler.onTimedMetadata(it) } + private fun finishReport(report: PendingTimedMetadata, startTime: Double) { + report.finish()?.let { metadata -> callback.onTimedMetadata(metadata, startTime) } } private val onAddTrack = EventListener { handleTrackAdded(it.track) } @@ -99,7 +97,7 @@ class TimedMetadataHandler( } } -private data class YospaceReport( +private data class PendingTimedMetadata( var ymid: String? = null, var ytyp: String? = null, var yseq: String? = null, @@ -114,17 +112,28 @@ private data class YospaceReport( } } - fun finish(playhead: Long): TimedMetadata? { + fun finish(): TimedMetadata? { val (ymid, ytyp, yseq, ydur) = this // Only create complete reports return if (ymid != null && ydur != null && yseq != null && ytyp != null) { - TimedMetadata.createFromMetadata(ymid, yseq, ytyp, ydur, playhead) + TimedMetadata(ymid = ymid, yseq = yseq, ytyp = ytyp, ydur = ydur) } else { null } } } +internal data class TimedMetadata( + val ymid: String, + val ytyp: String, + val yseq: String, + val ydur: String, +) + +internal fun interface TimedMetadataCallback { + fun onTimedMetadata(yospaceMetadata: TimedMetadata, startTime: Double) +} + private fun isYospaceId3Cue(cue: TextTrackCue): Boolean { val id3 = cue.content?.optJSONObject("content") ?: return false return when (id3.optString("id")) { diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 1aac649d..18bd6f55 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -26,6 +26,7 @@ import com.yospace.admanagement.Session import com.yospace.admanagement.SessionDVRLive import com.yospace.admanagement.SessionLive import com.yospace.admanagement.SessionVOD +import com.yospace.admanagement.TimedMetadata import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.math.max @@ -109,7 +110,7 @@ class YospaceAdIntegration( private fun setupSession(session: Session) { this.session = session - timedMetadataHandler = TimedMetadataHandler(player, session) + timedMetadataHandler = TimedMetadataHandler(player, onTimedMetadata) adHandler = AdHandler(controller).also { session.addAnalyticObserver(it) } @@ -203,6 +204,11 @@ class YospaceAdIntegration( updatePlayhead() } + private val onTimedMetadata = TimedMetadataCallback { metadata, startTime -> + val playhead = (startTime * 1000.0).toLong() + session?.onTimedMetadata(TimedMetadata.createFromMetadata(metadata.ymid, metadata.yseq, metadata.ytyp, metadata.ydur, playhead)) + } + private fun updatePlayhead() { val playhead = currentPlayhead session?.onPlayheadUpdate(playhead) From 01002b426d1f72b5ffabaf168890309a1d1bec5b Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 15:01:39 +0200 Subject: [PATCH 31/71] Convert timed metadata timestamps to playhead --- .../yospace/internal/YospaceAdIntegration.kt | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 18bd6f55..b5b181c4 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -45,17 +45,7 @@ class YospaceAdIntegration( private var isStalling: Boolean = false private val currentPlayhead: Long - get() { - var playhead = (player.currentTime * 1000.0).toLong() - - // For Yospace DVRLive sessions we need to offset the playback position from the stream start time - val session = this.session - if (session is SessionDVRLive) { - playhead += session.windowStart - max(session.streamStart, 0) - } - - return playhead - } + get() = toPlayhead(player.currentTime) override suspend fun setSource(source: SourceDescription): SourceDescription { val yospaceSource = source.sources.find { it.ssai is YospaceSsaiDescription } ?: return source @@ -205,10 +195,22 @@ class YospaceAdIntegration( } private val onTimedMetadata = TimedMetadataCallback { metadata, startTime -> - val playhead = (startTime * 1000.0).toLong() + val playhead = toPlayhead(startTime) session?.onTimedMetadata(TimedMetadata.createFromMetadata(metadata.ymid, metadata.yseq, metadata.ytyp, metadata.ydur, playhead)) } + private fun toPlayhead(playerTime: Double): Long { + var playhead = (playerTime * 1000.0).toLong() + + // For Yospace DVRLive sessions we need to offset the playback position from the stream start time + val session = this.session + if (session is SessionDVRLive) { + playhead += session.windowStart - max(session.streamStart, 0) + } + + return playhead + } + private fun updatePlayhead() { val playhead = currentPlayhead session?.onPlayheadUpdate(playhead) From a3d78671c46f83024cfee149b3df240e4ca1b784 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 15:04:07 +0200 Subject: [PATCH 32/71] Make internal --- .../theoplayer/android/connector/yospace/internal/AdHandler.kt | 2 +- .../android/connector/yospace/internal/YospaceAdIntegration.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt index 67122ba9..66245810 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt @@ -14,7 +14,7 @@ import kotlin.math.max import com.yospace.admanagement.AdBreak as YospaceAdBreak import com.yospace.admanagement.Advert as YospaceAdvert -class AdHandler(private val controller: ServerSideAdIntegrationController) : AnalyticEventObserver { +internal class AdHandler(private val controller: ServerSideAdIntegrationController) : AnalyticEventObserver { private val ads: WeakHashMap = WeakHashMap() private val adBreaks: WeakHashMap = WeakHashMap() private var currentAd: Ad? = null diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index b5b181c4..4d8e5364 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -31,7 +31,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.math.max -class YospaceAdIntegration( +internal class YospaceAdIntegration( private val player: Player, private val controller: ServerSideAdIntegrationController, private val analyticEventObserver: AnalyticEventObserver, From 6f45802ae0c54eb4cc4d29583632bafb7a95ece1 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 15:10:19 +0200 Subject: [PATCH 33/71] Fix Yospace module name in Dokka --- connectors/yospace/build.gradle | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index 15662774..f8a575e7 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -1,7 +1,10 @@ +import org.jetbrains.dokka.gradle.AbstractDokkaLeafTask + plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.plugin.serialization' + id 'org.jetbrains.dokka' } apply from: "$rootDir/connectors/publish.gradle" @@ -51,4 +54,8 @@ dependencies { testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion" testImplementation "com.yospace:admanagement-sdk:$yospaceVersion" -} \ No newline at end of file +} + +tasks.withType(AbstractDokkaLeafTask.class).configureEach { + moduleName = "Yospace Connector" +} From c26e90ff7fbd038933038f7350ef34ac1912601e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 15:44:20 +0200 Subject: [PATCH 34/71] Tweak --- connectors/yospace/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle index f8a575e7..3ca430db 100644 --- a/connectors/yospace/build.gradle +++ b/connectors/yospace/build.gradle @@ -8,8 +8,6 @@ plugins { } apply from: "$rootDir/connectors/publish.gradle" -version = sdkVersion - android { namespace 'com.theoplayer.android.connector.yospace' compileSdk 34 From 9ca82c41fae6dc30d3e5ad90aa0cf8157201db69 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 15:56:44 +0200 Subject: [PATCH 35/71] Remove unnecessary @Serializable --- .../android/connector/yospace/YospaceSsaiDescription.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt index a2e0bd1a..37e64680 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceSsaiDescription.kt @@ -59,7 +59,6 @@ class YospaceSsaiDescription( /** * The type of the Yospace stream. */ -@Serializable enum class YospaceStreamType { /** * The stream is a live stream. From 1882403b7ff5fe0d299c61dfbb02426db2693693 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 16:00:36 +0200 Subject: [PATCH 36/71] Tweak --- .../theoplayer/android/connector/yospace/internal/AdHandler.kt | 3 +-- .../android/connector/yospace/internal/YospaceAdIntegration.kt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt index 66245810..eedf7781 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt @@ -10,7 +10,6 @@ import com.yospace.admanagement.Resource import com.yospace.admanagement.Session import com.yospace.admanagement.TrackingErrors import java.util.WeakHashMap -import kotlin.math.max import com.yospace.admanagement.AdBreak as YospaceAdBreak import com.yospace.admanagement.Advert as YospaceAdvert @@ -115,7 +114,7 @@ internal class AdHandler(private val controller: ServerSideAdIntegrationControll val advert = this.currentYospaceAdvert ?: return val duration = advert.duration val remainingTime = advert.getRemainingTime(playhead) - val progress = max(0.0, (duration - remainingTime).toDouble() / duration.toDouble()) + val progress = ((duration - remainingTime).toDouble() / duration.toDouble()).coerceIn(0.0, 1.0) controller.updateAdProgress(ad, progress) } diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 4d8e5364..7f627455 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -29,7 +29,6 @@ import com.yospace.admanagement.SessionVOD import com.yospace.admanagement.TimedMetadata import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -import kotlin.math.max internal class YospaceAdIntegration( private val player: Player, @@ -205,7 +204,7 @@ internal class YospaceAdIntegration( // For Yospace DVRLive sessions we need to offset the playback position from the stream start time val session = this.session if (session is SessionDVRLive) { - playhead += session.windowStart - max(session.streamStart, 0) + playhead += session.windowStart - session.streamStart.coerceAtLeast(0) } return playhead From 6788f493f468f08a3f6dea491f29cf1f8598c8fb Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 16:59:00 +0200 Subject: [PATCH 37/71] Rework stream start computation --- .../yospace/internal/YospaceAdIntegration.kt | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 7f627455..2263be83 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -3,6 +3,7 @@ package com.theoplayer.android.connector.yospace.internal import android.util.Log import com.theoplayer.android.api.ads.ServerSideAdIntegrationController import com.theoplayer.android.api.ads.ServerSideAdIntegrationHandler +import com.theoplayer.android.api.event.Event import com.theoplayer.android.api.event.EventListener import com.theoplayer.android.api.event.player.EndedEvent import com.theoplayer.android.api.event.player.PauseEvent @@ -40,6 +41,7 @@ internal class YospaceAdIntegration( private var timedMetadataHandler: TimedMetadataHandler? = null private var adHandler: AdHandler? = null private var didFirstPlay: Boolean = false + private var streamStart: Double? = null private var isMuted: Boolean = false private var isStalling: Boolean = false @@ -135,6 +137,8 @@ internal class YospaceAdIntegration( player.addEventListener(PlayerEventTypes.WAITING, onWaiting) player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) + player.addEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) + player.addEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) } private fun removePlayerListeners() { @@ -146,6 +150,8 @@ internal class YospaceAdIntegration( player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) + player.removeEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) + player.removeEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) } private val onVolumeChange = EventListener { @@ -189,7 +195,12 @@ internal class YospaceAdIntegration( } } + private val onSeekableChange = EventListener> { + updateStreamStart() + } + private val onTimeUpdate = EventListener { + updateStreamStart() updatePlayhead() } @@ -199,15 +210,8 @@ internal class YospaceAdIntegration( } private fun toPlayhead(playerTime: Double): Long { - var playhead = (playerTime * 1000.0).toLong() - - // For Yospace DVRLive sessions we need to offset the playback position from the stream start time - val session = this.session - if (session is SessionDVRLive) { - playhead += session.windowStart - session.streamStart.coerceAtLeast(0) - } - - return playhead + val relativeTime = playerTime - (streamStart ?: 0.0) + return (relativeTime * 1000.0).toLong() } private fun updatePlayhead() { @@ -216,9 +220,22 @@ internal class YospaceAdIntegration( adHandler?.onTimeUpdate(playhead) } + private fun updateStreamStart() { + if (streamStart != null) return + val seekable = player.seekable + if (seekable.length() > 0) { + val seekableStart = seekable.getStart(0) + val seekableEnd = seekable.getEnd(0) + if (seekableStart < seekableEnd || seekableEnd > 0.0) { + streamStart = seekableStart + } + } + } + override suspend fun resetSource() { destroySession() didFirstPlay = false + streamStart = null isStalling = false } From bf82fbd8550fe50af1724a3eaa249d67bcf07a82 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 17:32:01 +0200 Subject: [PATCH 38/71] Fix permissions in sample app --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 24da06a1..f40ce51f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + Date: Wed, 29 May 2024 17:41:25 +0200 Subject: [PATCH 39/71] Log ad events in sample app --- .../android/connector/MainActivity.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) 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 9d340b22..ed607f56 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -1,6 +1,7 @@ package com.theoplayer.android.connector import android.os.Bundle +import android.util.Log import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -9,7 +10,11 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.theoplayer.android.api.THEOplayerConfig import com.theoplayer.android.api.THEOplayerView +import com.theoplayer.android.api.ads.LinearAd import com.theoplayer.android.api.ads.ima.GoogleImaIntegrationFactory +import com.theoplayer.android.api.event.ads.AdBreakEvent +import com.theoplayer.android.api.event.ads.AdsEventTypes +import com.theoplayer.android.api.event.ads.SingleAdEvent import com.theoplayer.android.connector.analytics.comscore.ComscoreConfiguration import com.theoplayer.android.connector.analytics.comscore.ComscoreConnector import com.theoplayer.android.connector.analytics.comscore.ComscoreMediaType @@ -19,6 +24,8 @@ import com.theoplayer.android.connector.analytics.conviva.ConvivaConnector import com.theoplayer.android.connector.analytics.nielsen.NielsenConnector import com.theoplayer.android.connector.yospace.YospaceConnector +const val TAG = "MainActivity" + class MainActivity : AppCompatActivity() { private lateinit var theoplayerView: THEOplayerView @@ -38,6 +45,7 @@ class MainActivity : AppCompatActivity() { setupComscore() setupNielsen() setupYospace() + setupListeners() } private fun setupComscore() { @@ -135,6 +143,44 @@ class MainActivity : AppCompatActivity() { yospaceConnector = YospaceConnector(theoplayerView.player) } + private fun setupListeners() { + val ads = theoplayerView.player.ads + ads.addEventListener(AdsEventTypes.ADD_AD, ::onAdEvent) + ads.addEventListener(AdsEventTypes.AD_BEGIN, ::onAdEvent) + ads.addEventListener(AdsEventTypes.AD_END, ::onAdEvent) + ads.addEventListener(AdsEventTypes.AD_SKIP, ::onAdEvent) + ads.addEventListener(AdsEventTypes.AD_FIRST_QUARTILE, ::onAdEvent) + ads.addEventListener(AdsEventTypes.AD_MIDPOINT, ::onAdEvent) + ads.addEventListener(AdsEventTypes.AD_THIRD_QUARTILE, ::onAdEvent) + ads.addEventListener(AdsEventTypes.ADD_AD_BREAK, ::onAdBreakEvent) + ads.addEventListener(AdsEventTypes.AD_BREAK_BEGIN, ::onAdBreakEvent) + ads.addEventListener(AdsEventTypes.AD_BREAK_END, ::onAdBreakEvent) + ads.addEventListener(AdsEventTypes.AD_BREAK_CHANGE, ::onAdBreakEvent) + ads.addEventListener(AdsEventTypes.REMOVE_AD_BREAK, ::onAdBreakEvent) + } + + fun onAdBreakEvent(event: AdBreakEvent<*>) { + val adBreak = event.adBreak ?: return + Log.d( + TAG, + "${event.type} - " + + "timeOffset=${adBreak.timeOffset}, ads=${adBreak.ads.size}," + + " maxDuration=${adBreak.maxDuration}," + + " currentTime=${theoplayerView.player.currentTime}" + ) + } + + fun onAdEvent(event: SingleAdEvent<*>) { + val ad = event.ad ?: return + Log.d( + TAG, + "${event.type} - " + + "id=${ad.id}, type=${ad.type}, adBreak.timeOffset=${ad.adBreak?.timeOffset}," + + (if (ad is LinearAd) " duration=${ad.duration}," else "") + + " currentTime=${theoplayerView.player.currentTime}" + ) + } + fun selectSource(view: View) { val sourceNames = sources.map { it.name }.toTypedArray() val selectedIndex = sources.indexOf(selectedSource) From 95def4c3b66855cd47658a9abb0458f0340058b4 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 17:50:55 +0200 Subject: [PATCH 40/71] Use timed metadata only for live playback --- .../yospace/internal/YospaceAdIntegration.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 2263be83..3f762e80 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -100,13 +100,18 @@ internal class YospaceAdIntegration( } private fun setupSession(session: Session) { + val isLive = session.playbackMode == Session.PlaybackMode.LIVE this.session = session - timedMetadataHandler = TimedMetadataHandler(player, onTimedMetadata) + if (isLive) { + // Timed metadata is only used for live playback + // https://developer.yospace.com/sdk-documentation/android/userguide/latest/en/provide-necessary-information-to-the-sdk.html#video-playback-position + timedMetadataHandler = TimedMetadataHandler(player, onTimedMetadata) + } adHandler = AdHandler(controller).also { session.addAnalyticObserver(it) } session.addAnalyticObserver(analyticEventObserver) - addPlayerListeners() + addPlayerListeners(isLive) updatePlayhead() } @@ -128,7 +133,7 @@ internal class YospaceAdIntegration( } } - private fun addPlayerListeners() { + private fun addPlayerListeners(isLive: Boolean) { player.addEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange) player.addEventListener(PlayerEventTypes.PLAY, onPlay) player.addEventListener(PlayerEventTypes.ENDED, onEnded) @@ -136,9 +141,13 @@ internal class YospaceAdIntegration( player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) player.addEventListener(PlayerEventTypes.WAITING, onWaiting) player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) player.addEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) player.addEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) + if (!isLive) { + // Playhead position is only used for DVR live and VOD playback + // https://developer.yospace.com/sdk-documentation/android/userguide/latest/en/provide-necessary-information-to-the-sdk.html#video-playback-position + player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) + } } private fun removePlayerListeners() { @@ -149,9 +158,9 @@ internal class YospaceAdIntegration( player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) player.removeEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) player.removeEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) + player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) } private val onVolumeChange = EventListener { From 6c4a6fea967ac21a50b16fa410fa12bfa758069a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 17:52:50 +0200 Subject: [PATCH 41/71] Remove listeners once streamStart is known --- .../yospace/internal/YospaceAdIntegration.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 3f762e80..30f1b1b8 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -134,6 +134,7 @@ internal class YospaceAdIntegration( } private fun addPlayerListeners(isLive: Boolean) { + addStreamStartListeners() player.addEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange) player.addEventListener(PlayerEventTypes.PLAY, onPlay) player.addEventListener(PlayerEventTypes.ENDED, onEnded) @@ -141,8 +142,6 @@ internal class YospaceAdIntegration( player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) player.addEventListener(PlayerEventTypes.WAITING, onWaiting) player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.addEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) - player.addEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) if (!isLive) { // Playhead position is only used for DVR live and VOD playback // https://developer.yospace.com/sdk-documentation/android/userguide/latest/en/provide-necessary-information-to-the-sdk.html#video-playback-position @@ -151,6 +150,7 @@ internal class YospaceAdIntegration( } private fun removePlayerListeners() { + removeStreamStartListeners() player.removeEventListener(PlayerEventTypes.VOLUMECHANGE, onVolumeChange) player.removeEventListener(PlayerEventTypes.PLAY, onPlay) player.removeEventListener(PlayerEventTypes.ENDED, onEnded) @@ -158,11 +158,21 @@ internal class YospaceAdIntegration( player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.removeEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) - player.removeEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) } + private fun addStreamStartListeners() { + player.addEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) + player.addEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) + player.addEventListener(PlayerEventTypes.TIMEUPDATE, onSeekableChange) + } + + private fun removeStreamStartListeners() { + player.addEventListener(PlayerEventTypes.LOADEDMETADATA, onSeekableChange) + player.addEventListener(PlayerEventTypes.DURATIONCHANGE, onSeekableChange) + player.addEventListener(PlayerEventTypes.TIMEUPDATE, onSeekableChange) + } + private val onVolumeChange = EventListener { val muted = player.isMuted if (isMuted != muted) { @@ -209,7 +219,6 @@ internal class YospaceAdIntegration( } private val onTimeUpdate = EventListener { - updateStreamStart() updatePlayhead() } @@ -237,6 +246,7 @@ internal class YospaceAdIntegration( val seekableEnd = seekable.getEnd(0) if (seekableStart < seekableEnd || seekableEnd > 0.0) { streamStart = seekableStart + removeStreamStartListeners() } } } From 63324139e3418161adfc30d99f4b74a72b9db1fd Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 18:00:19 +0200 Subject: [PATCH 42/71] Tweak --- .../yospace/internal/YospaceAdIntegration.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 30f1b1b8..6cd0d62f 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -241,13 +241,12 @@ internal class YospaceAdIntegration( private fun updateStreamStart() { if (streamStart != null) return val seekable = player.seekable - if (seekable.length() > 0) { - val seekableStart = seekable.getStart(0) - val seekableEnd = seekable.getEnd(0) - if (seekableStart < seekableEnd || seekableEnd > 0.0) { - streamStart = seekableStart - removeStreamStartListeners() - } + if (seekable.length() <= 0) return + val seekableStart = seekable.getStart(0) + val seekableEnd = seekable.getEnd(0) + if (seekableStart < seekableEnd || seekableEnd > 0.0) { + streamStart = seekableStart + removeStreamStartListeners() } } From 95798a24daaedadfff621447b01a4506241be51b Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 18:25:45 +0200 Subject: [PATCH 43/71] Handle ad breaks without information during live playback --- .../android/connector/yospace/internal/AdHandler.kt | 13 +++++++++++-- .../yospace/internal/YospaceAdIntegration.kt | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt index eedf7781..25ee0990 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt @@ -5,6 +5,7 @@ import com.theoplayer.android.api.ads.AdBreak import com.theoplayer.android.api.ads.AdBreakInit import com.theoplayer.android.api.ads.AdInit import com.theoplayer.android.api.ads.ServerSideAdIntegrationController +import com.theoplayer.android.api.player.Player import com.yospace.admanagement.AnalyticEventObserver import com.yospace.admanagement.Resource import com.yospace.admanagement.Session @@ -13,7 +14,10 @@ import java.util.WeakHashMap import com.yospace.admanagement.AdBreak as YospaceAdBreak import com.yospace.admanagement.Advert as YospaceAdvert -internal class AdHandler(private val controller: ServerSideAdIntegrationController) : AnalyticEventObserver { +internal class AdHandler( + private val player: Player, + private val controller: ServerSideAdIntegrationController +) : AnalyticEventObserver { private val ads: WeakHashMap = WeakHashMap() private val adBreaks: WeakHashMap = WeakHashMap() private var currentAd: Ad? = null @@ -56,7 +60,12 @@ internal class AdHandler(private val controller: ServerSideAdIntegrationControll } override fun onAdvertBreakStart(adBreak: YospaceAdBreak?, session: Session) { - currentAdBreak = getOrCreateAdBreak(adBreak ?: return) + currentAdBreak = if (adBreak == null) { + // During live playback, an ad break may be started without any information + controller.createAdBreak(AdBreakInit(timeOffset = player.currentTime.toInt())) + } else { + getOrCreateAdBreak(adBreak) + } } override fun onAdvertBreakEnd(session: Session) { diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 6cd0d62f..0b4675dd 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -107,7 +107,7 @@ internal class YospaceAdIntegration( // https://developer.yospace.com/sdk-documentation/android/userguide/latest/en/provide-necessary-information-to-the-sdk.html#video-playback-position timedMetadataHandler = TimedMetadataHandler(player, onTimedMetadata) } - adHandler = AdHandler(controller).also { + adHandler = AdHandler(player, controller).also { session.addAnalyticObserver(it) } session.addAnalyticObserver(analyticEventObserver) From 9ac0c15273b01734f282f2d9bf499f18a1fa7fd2 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 29 May 2024 18:48:52 +0200 Subject: [PATCH 44/71] Remove ad.timeOffset, since we always create an ad break --- .../theoplayer/android/connector/yospace/internal/AdHandler.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt index 25ee0990..626d78b1 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt @@ -50,7 +50,6 @@ internal class AdHandler( val nonLinearCreative = if (advert.isNonLinear) advert.getNonLinearCreatives(Resource.ResourceType.STATIC).firstOrNull() else null return AdInit( type = if (advert.isNonLinear) "nonlinear" else "linear", - timeOffset = (advert.start / 1000).toInt(), skipOffset = (advert.skipOffset / 1000).toInt(), id = advert.identifier, duration = (advert.duration / 1000).toInt(), From a5bdd3c9d5d0cb734e68f258d283e1656793d50c Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 30 May 2024 11:48:51 +0200 Subject: [PATCH 45/71] Convert AdBreak.timeOffset from playhead time --- .../android/connector/yospace/internal/AdHandler.kt | 8 ++++++-- .../connector/yospace/internal/PlayheadConverter.kt | 7 +++++++ .../yospace/internal/YospaceAdIntegration.kt | 11 ++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/PlayheadConverter.kt diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt index 626d78b1..89bddf20 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt @@ -16,7 +16,8 @@ import com.yospace.admanagement.Advert as YospaceAdvert internal class AdHandler( private val player: Player, - private val controller: ServerSideAdIntegrationController + private val controller: ServerSideAdIntegrationController, + private val playheadConverter: PlayheadConverter ) : AnalyticEventObserver { private val ads: WeakHashMap = WeakHashMap() private val adBreaks: WeakHashMap = WeakHashMap() @@ -31,7 +32,10 @@ internal class AdHandler( } private fun getAdBreakInit(yospaceAdBreak: YospaceAdBreak): AdBreakInit { - return AdBreakInit(timeOffset = (yospaceAdBreak.start / 1000).toInt(), maxDuration = (yospaceAdBreak.duration / 1000)) + return AdBreakInit( + timeOffset = playheadConverter.fromPlayhead(yospaceAdBreak.start).toInt(), + maxDuration = yospaceAdBreak.duration / 1000 + ) } private fun getOrCreateAd(yospaceAdvert: YospaceAdvert, adBreak: AdBreak): Ad { diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/PlayheadConverter.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/PlayheadConverter.kt new file mode 100644 index 00000000..503d3e88 --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/PlayheadConverter.kt @@ -0,0 +1,7 @@ +package com.theoplayer.android.connector.yospace.internal + +internal interface PlayheadConverter { + fun fromPlayhead(playhead: Long): Double + + fun toPlayhead(playerTime: Double): Long +} \ No newline at end of file diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 0b4675dd..673bbc96 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -36,7 +36,7 @@ internal class YospaceAdIntegration( private val controller: ServerSideAdIntegrationController, private val analyticEventObserver: AnalyticEventObserver, private val listener: YospaceListener -) : ServerSideAdIntegrationHandler { +) : ServerSideAdIntegrationHandler, PlayheadConverter { private var session: Session? = null private var timedMetadataHandler: TimedMetadataHandler? = null private var adHandler: AdHandler? = null @@ -107,7 +107,7 @@ internal class YospaceAdIntegration( // https://developer.yospace.com/sdk-documentation/android/userguide/latest/en/provide-necessary-information-to-the-sdk.html#video-playback-position timedMetadataHandler = TimedMetadataHandler(player, onTimedMetadata) } - adHandler = AdHandler(player, controller).also { + adHandler = AdHandler(player, controller, this).also { session.addAnalyticObserver(it) } session.addAnalyticObserver(analyticEventObserver) @@ -227,7 +227,12 @@ internal class YospaceAdIntegration( session?.onTimedMetadata(TimedMetadata.createFromMetadata(metadata.ymid, metadata.yseq, metadata.ytyp, metadata.ydur, playhead)) } - private fun toPlayhead(playerTime: Double): Long { + override fun fromPlayhead(playhead: Long): Double { + val relativeTime = playhead.toDouble() / 1000.0 + return relativeTime + (streamStart ?: 0.0) + } + + override fun toPlayhead(playerTime: Double): Long { val relativeTime = playerTime - (streamStart ?: 0.0) return (relativeTime * 1000.0).toLong() } From 9364f47e3a770c1293ac2cf68f93864000e5b60f Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 30 May 2024 11:49:06 +0200 Subject: [PATCH 46/71] Fix Ad.skipOffset --- .../theoplayer/android/connector/yospace/internal/AdHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt index 89bddf20..e4e5e0d4 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/AdHandler.kt @@ -54,7 +54,7 @@ internal class AdHandler( val nonLinearCreative = if (advert.isNonLinear) advert.getNonLinearCreatives(Resource.ResourceType.STATIC).firstOrNull() else null return AdInit( type = if (advert.isNonLinear) "nonlinear" else "linear", - skipOffset = (advert.skipOffset / 1000).toInt(), + skipOffset = if (advert.skipOffset < 0) -1 else (advert.skipOffset / 1000).toInt(), id = advert.identifier, duration = (advert.duration / 1000).toInt(), clickThrough = if (advert.isNonLinear) nonLinearCreative?.clickThroughUrl else advert.linearCreative?.clickThroughUrl, From c6790a4f4425454d4d7867d06dc8ee2b3f974e55 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 30 May 2024 13:10:00 +0200 Subject: [PATCH 47/71] Require THEOplayerView in YospaceConnector --- .../java/com/theoplayer/android/connector/MainActivity.kt | 2 +- .../android/connector/yospace/YospaceConnector.kt | 8 ++++---- .../connector/yospace/internal/YospaceAdIntegration.kt | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) 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 ed607f56..9a8532f8 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -140,7 +140,7 @@ class MainActivity : AppCompatActivity() { } private fun setupYospace() { - yospaceConnector = YospaceConnector(theoplayerView.player) + yospaceConnector = YospaceConnector(theoplayerView) } private fun setupListeners() { diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt index 7919205f..458debaf 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt @@ -1,7 +1,7 @@ package com.theoplayer.android.connector.yospace +import com.theoplayer.android.api.THEOplayerView import com.theoplayer.android.api.ads.ServerSideAdIntegrationController -import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.source.ssai.CustomSsaiDescriptionRegistry import com.theoplayer.android.connector.yospace.internal.YospaceAdIntegration import com.yospace.admanagement.AdBreak @@ -16,7 +16,7 @@ internal const val TAG = "YospaceConnector" internal const val USER_AGENT = "THEOplayerYospaceConnector/${BuildConfig.LIBRARY_VERSION}" class YospaceConnector( - val player: Player + private val theoplayerView: THEOplayerView ) { private val analyticEventObservers = CopyOnWriteArrayList() private val listeners = CopyOnWriteArrayList() @@ -24,12 +24,12 @@ class YospaceConnector( private lateinit var integration: YospaceAdIntegration init { - player.ads.registerServerSideIntegration(INTEGRATION_ID, this::setupIntegration) + theoplayerView.player.ads.registerServerSideIntegration(INTEGRATION_ID, this::setupIntegration) } private fun setupIntegration(controller: ServerSideAdIntegrationController): YospaceAdIntegration { val integration = YospaceAdIntegration( - player, + theoplayerView, controller, ForwardingAnalyticEventObserver(), ForwardingListener() diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt index 673bbc96..eba30537 100644 --- a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/internal/YospaceAdIntegration.kt @@ -1,6 +1,7 @@ package com.theoplayer.android.connector.yospace.internal import android.util.Log +import com.theoplayer.android.api.THEOplayerView import com.theoplayer.android.api.ads.ServerSideAdIntegrationController import com.theoplayer.android.api.ads.ServerSideAdIntegrationHandler import com.theoplayer.android.api.event.Event @@ -32,7 +33,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine internal class YospaceAdIntegration( - private val player: Player, + private val theoplayerView: THEOplayerView, private val controller: ServerSideAdIntegrationController, private val analyticEventObserver: AnalyticEventObserver, private val listener: YospaceListener @@ -45,6 +46,9 @@ internal class YospaceAdIntegration( private var isMuted: Boolean = false private var isStalling: Boolean = false + private val player: Player + get() = theoplayerView.player + private val currentPlayhead: Long get() = toPlayhead(player.currentTime) From 73136a23ed6dfaca83653f7c7a965d9a402beb68 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 30 May 2024 20:03:43 +0200 Subject: [PATCH 48/71] Tweak gradle.properties --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index cf0c124e..ee33ca47 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,8 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=true groupId=com.theoplayer.android-connector sdkVersion=7.6.0 @@ -29,5 +31,3 @@ lifecycleVersion=2.5.1 convivaVersion=4.0.35 comscoreVersion=6.10.0 yospaceVersion=3.6.7 -android.defaults.buildfeatures.buildconfig=true -android.nonFinalResIds=false \ No newline at end of file From 45ad962321cacba40e7ce2d9aa9080c6ec6b508c Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Thu, 30 May 2024 20:03:05 +0200 Subject: [PATCH 49/71] Add ads UI --- .../yospace/DefaultYospaceUiHandler.kt | 35 +++++++++++++++++++ .../connector/yospace/YospaceConnector.kt | 6 ++-- .../connector/yospace/YospaceUiHandler.kt | 16 +++++++++ .../connector/yospace/internal/AdHandler.kt | 26 +++++++++++--- .../yospace/internal/YospaceAdIntegration.kt | 5 ++- .../yospace/src/main/res/layout/ads.xml | 14 ++++++++ .../yospace/src/main/res/values/ids.xml | 6 ++++ .../yospace/src/main/res/values/strings.xml | 4 +++ 8 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/DefaultYospaceUiHandler.kt create mode 100644 connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceUiHandler.kt create mode 100644 connectors/yospace/src/main/res/layout/ads.xml create mode 100644 connectors/yospace/src/main/res/values/ids.xml create mode 100644 connectors/yospace/src/main/res/values/strings.xml diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/DefaultYospaceUiHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/DefaultYospaceUiHandler.kt new file mode 100644 index 00000000..83f07e1a --- /dev/null +++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/DefaultYospaceUiHandler.kt @@ -0,0 +1,35 @@ +package com.theoplayer.android.connector.yospace + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import com.theoplayer.android.api.THEOplayerView +import com.yospace.admanagement.LinearCreative + +class DefaultYospaceUiHandler( + theoplayerView: THEOplayerView +) : YospaceUiHandler { + private val parentView: ViewGroup = theoplayerView.findViewById(R.id.theo_ads_container) + private val adsContainer: ViewGroup = (LayoutInflater.from(parentView.context).inflate(R.layout.ads, parentView, false) as ViewGroup).also { + parentView.addView(it) + } + private val linearClickThroughButton = adsContainer.findViewById