Skip to content

Commit

Permalink
Merge pull request #26 from THEOplayer/feature/uplink-preplay-events
Browse files Browse the repository at this point in the history
Uplynk connector: Preplay events
  • Loading branch information
OlegRyz authored Aug 26, 2024
2 parents 1c82186 + 63d8f18 commit 13709fc
Show file tree
Hide file tree
Showing 17 changed files with 593 additions and 31 deletions.
30 changes: 30 additions & 0 deletions app/src/main/java/com/theoplayer/android/connector/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.api.event.player.PlayerEventTypes
import com.theoplayer.android.connector.analytics.comscore.ComscoreConfiguration
import com.theoplayer.android.connector.analytics.comscore.ComscoreConnector
import com.theoplayer.android.connector.analytics.comscore.ComscoreMediaType
Expand All @@ -23,6 +24,9 @@ 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.uplynk.UplynkConnector
import com.theoplayer.android.connector.uplynk.UplynkListener
import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse
import com.theoplayer.android.connector.uplynk.network.PreplayResponse
import com.theoplayer.android.connector.yospace.YospaceConnector

const val TAG = "MainActivity"
Expand Down Expand Up @@ -148,6 +152,32 @@ class MainActivity : AppCompatActivity() {

private fun setupUplynk() {
uplynkConnector = UplynkConnector(theoplayerView)
uplynkConnector.addListener(object: UplynkListener {
override fun onPreplayResponse(response: PreplayResponse) {
Log.d("UplynkConnectorEvents", "PREPLAY_RESPONSE $response")
}

override fun onAssetInfoResponse(response: AssetInfoResponse) {
Log.d("UplynkConnectorEvents", "ASSET_INFO_RESPONSE $response")
}

override fun onPreplayFailure(exception: Exception) {
Log.d("UplynkConnectorEvents", "PREPLAY_RESPONSE_FAILURE $exception")
}

override fun onAssetInfoFailure(exception: Exception) {
Log.d("UplynkConnectorEvents", "ASSET_INFO_RESPONSE Failure $exception")
}

})

theoplayerView.player.ads.addEventListener(AdsEventTypes.AD_ERROR) {
Log.d("UplynkConnectorEvents", "AD_ERROR " + it.error)
}

theoplayerView.player.addEventListener(PlayerEventTypes.ERROR) {
Log.d("UplynkConnectorEvents", "ERROR " + it.errorObject)
}
}

private fun setupListeners() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ val sources: List<Source> by lazy {
UplynkSsaiDescription
.Builder()
.prefix("https://content.uplynk.com")
.assetInfo(true)
.assetIds(listOf(
"41afc04d34ad4cbd855db52402ef210e",
"c6b61470c27d44c4842346980ec2c7bd",
Expand Down
1 change: 1 addition & 0 deletions connectors/uplynk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
testImplementation "com.theoplayer.theoplayer-sdk-android:core:$sdkVersion"
testImplementation "org.mockito:mockito-inline:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.8.10"
}

tasks.withType(AbstractDokkaLeafTask.class).configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.theoplayer.android.api.ads.ServerSideAdIntegrationController
import com.theoplayer.android.api.source.ssai.CustomSsaiDescriptionRegistry
import com.theoplayer.android.connector.uplynk.internal.UplynkAdIntegration
import com.theoplayer.android.connector.uplynk.internal.UplynkSsaiDescriptionConverter
import com.theoplayer.android.connector.uplynk.internal.UplynkEventDispatcher
import com.theoplayer.android.connector.uplynk.internal.network.UplynkApi

internal const val TAG = "UplynkConnector"

Expand All @@ -21,6 +23,7 @@ class UplynkConnector(
private val theoplayerView: THEOplayerView,
) {
private lateinit var integration: UplynkAdIntegration
private val eventDispatcher = UplynkEventDispatcher()

init {
theoplayerView.player.ads.registerServerSideIntegration(INTEGRATION_ID, this::setupIntegration)
Expand All @@ -30,12 +33,18 @@ class UplynkConnector(
val integration = UplynkAdIntegration(
theoplayerView,
controller,
UplynkSsaiDescriptionConverter()
eventDispatcher,
UplynkSsaiDescriptionConverter(),
UplynkApi()
)
this.integration = integration
return integration
}

fun addListener(listener: UplynkListener) = eventDispatcher.addListener(listener)

fun removeListener(listener: UplynkListener) = eventDispatcher.removeListener(listener)

companion object {
/**
* The integration identifier for the Uplynk connector.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.theoplayer.android.connector.uplynk

import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse
import com.theoplayer.android.connector.uplynk.network.PreplayResponse

/**
* A listener interface for receiving events related to Uplynk
* Implementations of this interface can be used to handle responses from Uplynk's API.
*/
interface UplynkListener {

/**
* Called when a preplay response is received from Uplynk.
*
* For more details, refer to the [Preplay API (Version 2) Documentation](https://docs.edgecast.com/video/index.html#Develop/Preplayv2.htm).
*
* @param response the `PreplayResponse` object containing information relevant to the preplay request.
*/
fun onPreplayResponse(response: PreplayResponse) {}

/**
* Called when a preplay response is received from Uplynk and failed to be parsed
*
* @param exception the `Exception` occurred during the response parsing
*/
fun onPreplayFailure(exception: Exception) {}

/**
* Called when an asset information response is received from Uplynk.
*
* For more details, refer to the [Asset Info Documentation](https://docs.edgecast.com/video/index.html#Develop/AssetInfo.htm).
*
* @param response the `AssetInfoResponse` object containing detailed information about the asset.
*/
fun onAssetInfoResponse(response: AssetInfoResponse) {}

/**
* Called when an asset information response is failed
*
* @param exception the `Exception` occurred during the request
*/
fun onAssetInfoFailure(exception: Exception) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ data class UplynkSsaiDescription(
val assetIds: List<String> = listOf(),
val externalId: List<String> = listOf(),
val userId: String? = null,
val preplayParameters: LinkedHashMap<String, String> = linkedMapOf()
val preplayParameters: LinkedHashMap<String, String> = linkedMapOf(),
val assetInfo: Boolean = false
): CustomSsaiDescription() {

override val customIntegration: String
Expand Down Expand Up @@ -73,6 +74,11 @@ data class UplynkSsaiDescription(
*/
fun preplayParameters(parameters: LinkedHashMap<String, String>) = apply { this.preplayParameters = parameters }

private var assetInfo: Boolean = false
/**
* Sets flat to request asset info
*/
fun assetInfo(shouldRequest: Boolean) = apply { this.assetInfo = shouldRequest }
/**
* Builds the [UplynkSsaiDescription].
*/
Expand All @@ -81,6 +87,7 @@ data class UplynkSsaiDescription(
assetIds = assetIds,
externalId = externalIds,
userId = userId,
preplayParameters = preplayParameters)
preplayParameters = preplayParameters,
assetInfo = assetInfo)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,57 @@ import com.theoplayer.android.api.ads.ServerSideAdIntegrationHandler
import com.theoplayer.android.api.player.Player
import com.theoplayer.android.api.source.SourceDescription
import com.theoplayer.android.connector.uplynk.UplynkSsaiDescription
import com.theoplayer.android.connector.uplynk.network.UplynkApi
import com.theoplayer.android.connector.uplynk.internal.network.UplynkApi

internal class UplynkAdIntegration(
val theoplayerView: THEOplayerView,
val controller: ServerSideAdIntegrationController,
val uplynkDescriptionConverter: UplynkSsaiDescriptionConverter
private val theoplayerView: THEOplayerView,
private val controller: ServerSideAdIntegrationController,
private val eventDispatcher: UplynkEventDispatcher,
private val uplynkDescriptionConverter: UplynkSsaiDescriptionConverter,
private val uplynkApi: UplynkApi
) : ServerSideAdIntegrationHandler {

private val player: Player
get() = theoplayerView.player

private val uplynkApi = UplynkApi()

override suspend fun setSource(source: SourceDescription): SourceDescription {
val uplynkSource = source.sources.find { it.ssai is UplynkSsaiDescription } ?: return source
val ssaiDescription = uplynkSource.ssai as? UplynkSsaiDescription ?: return source
val response = uplynkApi.preplay(uplynkDescriptionConverter.buildPreplayUrl(ssaiDescription))

val uplynkSource = source.sources.singleOrNull { it.ssai is UplynkSsaiDescription }
val ssaiDescription = uplynkSource?.ssai as? UplynkSsaiDescription ?: return source

val response = uplynkDescriptionConverter
.buildPreplayUrl(ssaiDescription)
.let { uplynkApi.preplay(it) }
.also {
try {
eventDispatcher.dispatchPreplayEvents(it.parseExternalResponse())
} catch (e: Exception) {
eventDispatcher.dispatchPreplayFailure(e)
controller.error(e)
}
}

val internalResponse = response.parseMinimalResponse()

val newSource = source.replaceSources(source.sources.toMutableList().apply {
remove(uplynkSource)
add(0, uplynkSource.replaceSrc(response.playURL))
add(0, uplynkSource.replaceSrc(internalResponse.playURL))
})
if (ssaiDescription.assetInfo) {
uplynkDescriptionConverter
.buildAssetInfoUrls(ssaiDescription, internalResponse.sid)
.mapNotNull {
try {
uplynkApi.assetInfo(it)
} catch (e: Exception) {
eventDispatcher.dispatchAssetInfoFailure(e)
controller.error(e)
null
}
}
.forEach { eventDispatcher.dispatchAssetInfoEvents(it) }
}

return newSource
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.theoplayer.android.connector.uplynk.internal

import android.os.Handler
import android.os.Looper
import com.theoplayer.android.connector.uplynk.UplynkListener
import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse
import com.theoplayer.android.connector.uplynk.network.PreplayResponse
import java.util.concurrent.CopyOnWriteArrayList

internal class UplynkEventDispatcher {
private val handler = Handler(Looper.getMainLooper())

private val listeners = CopyOnWriteArrayList<UplynkListener>()

fun dispatchPreplayEvents(response: PreplayResponse) = handler.post {
listeners.forEach { it.onPreplayResponse(response) }
}

fun dispatchAssetInfoEvents(assetInfo: AssetInfoResponse) = handler.post {
listeners.forEach { it.onAssetInfoResponse(assetInfo) }
}

fun dispatchAssetInfoFailure(e: Exception) = handler.post {
listeners.forEach { it.onAssetInfoFailure(e) }
}

fun dispatchPreplayFailure(e: Exception) = handler.post {
listeners.forEach { it.onPreplayFailure(e) }
}

fun addListener(listener: UplynkListener) = listeners.add(listener)

fun removeListener(listener: UplynkListener) = listeners.remove(listener)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,22 @@ internal class UplynkSsaiDescriptionConverter {
val parameters = preplayParameters.map{ "${it.key}=${it.value}" }.joinToString("&")
return "$prefix/preplay/$assetIds?v=2&$parameters"
}

fun buildAssetInfoUrls(ssaiDescription: UplynkSsaiDescription, sessionId: String): List<String> = with(ssaiDescription) {
val prefix = prefix ?: DEFAULT_PREFIX
val urlList = when {
assetIds.isNotEmpty() -> assetIds.map {
"$prefix/player/assetinfo/$it.json"
}
externalId.isNotEmpty() -> externalId.map {
"$prefix/player/assetinfo/ext/$userId/$it.json"
}
else -> listOf()
}
return if (sessionId.isBlank()) {
urlList
} else {
urlList.map { "$it?pbs=$sessionId" }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.theoplayer.android.connector.uplynk.network
package com.theoplayer.android.connector.uplynk.internal.network

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
Expand Down Expand Up @@ -36,4 +36,4 @@ internal class HttpsConnection {
connection?.disconnect()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.theoplayer.android.connector.uplynk.internal.network

import com.theoplayer.android.connector.uplynk.network.PreplayResponse
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json


@Serializable
internal data class MinimalPreplayResponse(

/**
* The manifest's URL. (**NonNull**)
*/
val playURL: String,

/**
* The identifier of the viewer's session. (**NonNull**)
*/
val sid: String)


internal class PreplayInternalResponse(val body: String, private val json: Json) {
fun parseMinimalResponse(): MinimalPreplayResponse = json.decodeFromString(body)
fun parseExternalResponse(): PreplayResponse = json.decodeFromString(body)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.theoplayer.android.connector.uplynk.internal.network

import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse
import kotlinx.serialization.json.Json


internal class UplynkApi {
private val json = Json { ignoreUnknownKeys = true }
private val network = HttpsConnection()

suspend fun preplay(srcURL: String): PreplayInternalResponse {
val body = network.get(srcURL)
return PreplayInternalResponse(body, json)
}

suspend fun assetInfo(url: String): AssetInfoResponse {
val body = network.get(url)
return json.decodeFromString(body)
}
}
Loading

0 comments on commit 13709fc

Please sign in to comment.