diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 65ea9f36..68c760d7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,3 +27,6 @@ jobs:
uses: gradle/actions/setup-gradle@v3
- name: Run unit tests
run: ./gradlew testReleaseUnitTest
+ env:
+ YOSPACE_USERNAME: ${{ secrets.YOSPACE_USERNAME }}
+ YOSPACE_PASSWORD: ${{ secrets.YOSPACE_PASSWORD }}
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 60a23a08..c353be07 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -39,6 +39,9 @@ jobs:
uses: gradle/actions/setup-gradle@v3
- name: Build API documentation with Dokka
run: ./gradlew :dokkaHtmlMultiModule --no-configuration-cache
+ env:
+ YOSPACE_USERNAME: ${{ secrets.YOSPACE_USERNAME }}
+ YOSPACE_PASSWORD: ${{ secrets.YOSPACE_PASSWORD }}
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 5003a6f6..f00db872 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -33,3 +33,5 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSILITE_USERNAME: ${{ secrets.REPOSILITE_USERNAME }}
REPOSILITE_PASSWORD: ${{ secrets.REPOSILITE_PASSWORD }}
+ YOSPACE_USERNAME: ${{ secrets.YOSPACE_USERNAME }}
+ YOSPACE_PASSWORD: ${{ secrets.YOSPACE_PASSWORD }}
diff --git a/app/build.gradle b/app/build.gradle
index 1260da3f..db22bf71 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,6 +10,7 @@ android {
defaultConfig {
applicationId "com.theoplayer.android.connector"
minSdk 21
+ targetSdk 34
versionCode 1
versionName "1.0"
}
@@ -47,4 +48,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/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">
+
) {
+ val adBreak = event.adBreak
+ 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}"
)
- .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) {
@@ -177,4 +230,4 @@ class MainActivity : AppCompatActivity() {
super.onDestroy()
theoplayerView.onDestroy()
}
-}
\ No newline at end of file
+}
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..bba4a929
--- /dev/null
+++ b/app/src/main/java/com/theoplayer/android/connector/Sources.kt
@@ -0,0 +1,104 @@
+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,
+ 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"
+ )
+ ),
+ 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
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"/>
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()
}
}
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/README.md b/connectors/yospace/README.md
new file mode 100644
index 00000000..acd09a20
--- /dev/null
+++ b/connectors/yospace/README.md
@@ -0,0 +1,55 @@
+# THEOplayer Yospace Connector for Android
+
+The Yospace connector provides a Yospace integration for THEOplayer.
+
+## Installation
+
+1. Add the THEOplayer Maven repository to your project-level `settings.gradle` file:
+ ```groovy
+ dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url = uri("https://maven.theoplayer.com/releases/") }
+ }
+ }
+ ```
+2. Add the Yospace Maven repository to that same `settings.gradle` file.
+ Please refer to the [Yospace Ad Management SDK documentation][yospace-userguide] (section 7.2. Downloads > Ad Management SDK) for instructions.
+3. Add THEOplayer, the Yospace Ad Management SDK and the Yospace Connector as dependencies in your module-level `build.gradle` file:
+ ```groovy
+ dependencies {
+ implementation "com.theoplayer.theoplayer-sdk-android:core:7.+"
+ implementation "com.yospace:admanagement-sdk:3.+"
+ implementation "com.theoplayer.android-connector:yospace:7.+"
+ }
+ ```
+
+## Usage
+
+First, follow [the getting started guide for the THEOplayer Android SDK][android-getting-started]
+to set up a `THEOplayerView` in your app.
+
+Then, create the `YospaceConnector`:
+
+```kotlin
+import com.theoplayer.android.api.THEOplayerView
+import com.theoplayer.android.connector.yospace.YospaceConnector
+
+val theoPlayerView = findViewById(R.id.theoplayer)
+val yospaceConnector = YospaceConnector(theoPlayerView)
+```
+
+Finally, set the THEOplayer source to a `SourceDescription` with a `YospaceSsaiDescription` as its `ssai` description:
+```kotlin
+theoplayerView.player.source = SourceDescription.Builder(
+ TypedSource.Builder("https://example.com/stream.m3u8")
+ .ssai(
+ YospaceSsaiDescription(streamType = YospaceStreamType.LIVE)
+ )
+ .build()
+).build()
+```
+
+[yospace-userguide]: https://developer.yospace.com/sdk-documentation/android/userguide/latest/en/index-en.html
+[android-getting-started]: https://www.theoplayer.com/docs/theoplayer/getting-started/sdks/android/getting-started/
diff --git a/connectors/yospace/build.gradle b/connectors/yospace/build.gradle
new file mode 100644
index 00000000..0743e5dd
--- /dev/null
+++ b/connectors/yospace/build.gradle
@@ -0,0 +1,63 @@
+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"
+
+android {
+ namespace 'com.theoplayer.android.connector.yospace'
+ compileSdk 34
+
+ defaultConfig {
+ minSdk 21
+ consumerProguardFiles "consumer-rules.pro"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ buildConfigField("String", "LIBRARY_VERSION", "\"${sdkVersion}\"")
+ }
+
+ 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"
+ compileOnly("com.yospace:admanagement-sdk") {
+ version {
+ strictly("[3.6, 4.0)")
+ prefer(yospaceVersion)
+ }
+ }
+ 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:$yospaceVersion"
+}
+
+tasks.withType(AbstractDokkaLeafTask.class).configureEach {
+ moduleName = "Yospace Connector"
+
+ dokkaSourceSets.named("main") {
+ samples.from(project.fileTree("src/test/java/"))
+ }
+}
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/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..98732101
--- /dev/null
+++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceConnector.kt
@@ -0,0 +1,140 @@
+package com.theoplayer.android.connector.yospace
+
+import com.theoplayer.android.api.THEOplayerView
+import com.theoplayer.android.api.ads.Ad
+import com.theoplayer.android.api.ads.ServerSideAdIntegrationController
+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
+import com.yospace.admanagement.AnalyticEventObserver
+import com.yospace.admanagement.Session
+import com.yospace.admanagement.TrackingErrors
+import java.util.concurrent.CopyOnWriteArrayList
+
+internal const val TAG = "YospaceConnector"
+internal const val USER_AGENT = "THEOplayerYospaceConnector/${BuildConfig.LIBRARY_VERSION}"
+
+/**
+ * A connector for the Yospace Ad Management SDK.
+ *
+ * @param theoplayerView
+ * The THEOplayer view, which will be connected to the created connector.
+ * @param uiHandler
+ * A handler for updating the UI. By default, this creates a [YospaceDefaultUiHandler].
+ *
+ * @sample com.theoplayer.android.connector.yospace.samples.createYospaceConnector
+ *
+ * @see [Yospace Ad Management SDK v3 for Android Developers](https://developer.yospace.com/sdk-documentation/android/userguide/latest/en/index-en.html)
+ * (requires login)
+ */
+class YospaceConnector @JvmOverloads constructor(
+ private val theoplayerView: THEOplayerView,
+ private val uiHandler: YospaceUiHandler = YospaceDefaultUiHandler(theoplayerView)
+) {
+ private val analyticEventObservers = CopyOnWriteArrayList()
+ private val listeners = CopyOnWriteArrayList()
+
+ private lateinit var integration: YospaceAdIntegration
+
+ init {
+ theoplayerView.player.ads.registerServerSideIntegration(INTEGRATION_ID, this::setupIntegration)
+ }
+
+ private fun setupIntegration(controller: ServerSideAdIntegrationController): YospaceAdIntegration {
+ val integration = YospaceAdIntegration(
+ theoplayerView,
+ uiHandler,
+ controller,
+ ForwardingAnalyticEventObserver(),
+ ForwardingListener()
+ )
+ this.integration = integration
+ return integration
+ }
+
+ /**
+ * Registers an [AnalyticEventObserver] on the Yospace [Session].
+ */
+ fun registerAnalyticEventObserver(observer: AnalyticEventObserver) {
+ analyticEventObservers.add(observer)
+ }
+
+ /**
+ * Removes a previously registered [AnalyticEventObserver].
+ */
+ fun unregisterAnalyticEventObserver(observer: AnalyticEventObserver) {
+ analyticEventObservers.remove(observer)
+ }
+
+ /**
+ * Registers a [YospaceListener] to receive events from this connector.
+ */
+ fun addListener(listener: YospaceListener) {
+ listeners.add(listener)
+ }
+
+ /**
+ * Removes a previously registered [YospaceListener].
+ */
+ fun removeListener(listener: YospaceListener) {
+ 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() }
+ }
+ }
+
+ companion object {
+ /**
+ * The integration identifier for the Yospace connector.
+ *
+ * Ads created by this connector have this value as their [custom integration][Ad.getCustomIntegration].
+ */
+ const val INTEGRATION_ID = "yospace"
+
+ init {
+ CustomSsaiDescriptionRegistry.register(INTEGRATION_ID, YospaceSsaiDescriptionSerializer())
+ }
+ }
+}
diff --git a/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceDefaultUiHandler.kt b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceDefaultUiHandler.kt
new file mode 100644
index 00000000..9077bfd3
--- /dev/null
+++ b/connectors/yospace/src/main/java/com/theoplayer/android/connector/yospace/YospaceDefaultUiHandler.kt
@@ -0,0 +1,102 @@
+package com.theoplayer.android.connector.yospace
+
+import android.graphics.BitmapFactory
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import com.theoplayer.android.api.THEOplayerView
+import com.yospace.admanagement.LinearCreative
+import com.yospace.admanagement.NonLinearCreative
+import com.yospace.admanagement.Resource
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.net.URL
+
+/**
+ * A default UI handler for the Yospace connector.
+ *
+ * This handler shows a linear clickthrough button in the top-right corner,
+ * and shows clickable non-linear images in the top-left corner.
+ *
+ * If you want to customize the UI, you can implement your own [YospaceUiHandler]
+ * and pass it to the [YospaceConnector] constructor.
+ */
+class YospaceDefaultUiHandler(
+ 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.default_ad_overlay, parentView, false) as ViewGroup).also {
+ parentView.addView(it)
+ }
+ private val linearClickThroughButton = adsContainer.findViewById