Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uplynk Ping API #35

Merged
merged 25 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e6f1921
Add PingResponse
OlegRyz Sep 3, 2024
453ed6b
Add ping request
OlegRyz Sep 3, 2024
75b2bb4
Implement PingScheduler
OlegRyz Sep 3, 2024
8ccc77d
Differentiate between preplay vod and preplay live requests
OlegRyz Sep 4, 2024
689beb4
Update documentation
OlegRyz Sep 4, 2024
7c10750
Set default value for interstitialURL
OlegRyz Sep 5, 2024
6a909ab
Fix unit tests
OlegRyz Sep 5, 2024
6711d80
Cancel ping couroutines in PingScheduler.destroy()
OlegRyz Sep 5, 2024
654129b
Rename onPreplayLiveResponse to correspond to PreplayLiveResponse dat…
OlegRyz Sep 5, 2024
1417fc0
Add explanation for extensions field
OlegRyz Sep 5, 2024
2ca3306
Remove mistakenly added field. This field is not documented in Uplynk…
OlegRyz Sep 5, 2024
3f6da7f
Add missing documentation
OlegRyz Sep 6, 2024
af63190
Fix pingResponse javadoc
OlegRyz Sep 6, 2024
95b87a4
Reformat ping configuration features calculation
OlegRyz Sep 6, 2024
33c94f8
Move PingResponse out of internal pcackage
OlegRyz Sep 6, 2024
72710af
Add drm to PreplayLiveResponse
OlegRyz Sep 6, 2024
fbaed1b
Rename PreplayVodResponse
OlegRyz Sep 6, 2024
9dc8216
Add Uplynk Live source
OlegRyz Sep 6, 2024
ca47878
Enable Ping in Uplynk Live source
OlegRyz Sep 6, 2024
82305b6
Add Ping listener to main activity
OlegRyz Sep 6, 2024
0d77e3c
Enable ping scheduler only if ping feature is requested.
OlegRyz Sep 6, 2024
5666726
Make UplynkPingFeatures internal
OlegRyz Sep 6, 2024
fdf08d8
Rename PreplayInternalVodResponse
OlegRyz Sep 9, 2024
47644f1
Delete leftover empty file
OlegRyz Sep 9, 2024
0584cf6
Make externalIds property name plural
OlegRyz Sep 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.theoplayer.android.connector.uplynk

import com.theoplayer.android.connector.uplynk.internal.network.PingResponse
import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse
import com.theoplayer.android.connector.uplynk.network.PreplayLiveResponse
import com.theoplayer.android.connector.uplynk.network.PreplayResponse

/**
Expand All @@ -18,6 +20,15 @@ interface UplynkListener {
*/
fun onPreplayResponse(response: PreplayResponse) {}

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

/**
* Called when a preplay response is received from Uplynk and failed to be parsed
*
Expand All @@ -40,4 +51,13 @@ interface UplynkListener {
* @param exception the `Exception` occurred during the request
*/
fun onAssetInfoFailure(exception: Exception) {}

/**
* Called when a ping response is received from Uplynk.
*
* For more details, refer to the [Ping API Documentation](https://docs.edgecast.com/video/#Develop/Pingv2.htm).
*
* @param exception the `Exception` occurred during the request
hovig-theo marked this conversation as resolved.
Show resolved Hide resolved
*/
fun onPingResponse(pingResponse: PingResponse) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ data class UplynkSsaiDescription(
val userId: String? = null,
val contentProtected: Boolean = false,
val preplayParameters: LinkedHashMap<String, String> = linkedMapOf(),
val assetInfo: Boolean = false
): CustomSsaiDescription() {
val assetInfo: Boolean = false,
val assetType: UplynkAssetType = UplynkAssetType.ASSET,
val pingConfiguration: UplynkPingConfiguration = UplynkPingConfiguration()
hovig-theo marked this conversation as resolved.
Show resolved Hide resolved
) : CustomSsaiDescription() {

override val customIntegration: String
get() = UplynkConnector.INTEGRATION_ID
Expand All @@ -25,6 +27,7 @@ data class UplynkSsaiDescription(
*/
class Builder {
private var prefix: String? = null

/**
* Sets the prefix to use for Uplynk Media Platform Preplay API and Asset Info API requests.
*
Expand All @@ -35,6 +38,7 @@ data class UplynkSsaiDescription(
fun prefix(prefix: String) = apply { this.prefix = prefix }

private var assetIds = emptyList<String>()

/**
* Sets a list of asset IDs for Uplynk Media Platform Preplay API.
*
Expand All @@ -43,6 +47,7 @@ data class UplynkSsaiDescription(
fun assetIds(ids: List<String>) = apply { this.assetIds = ids }

private var externalIds: List<String> = emptyList<String>()

/**
* Sets a list of external IDs for Uplynk Media Platform Preplay API.
* If [assetIds] have at least one value this property is ignored and could be empty
Expand All @@ -52,6 +57,7 @@ data class UplynkSsaiDescription(
fun externalIds(ids: List<String>) = apply { this.externalIds = ids }

private var userId: String? = null

/**
* Sets a User ID for Uplynk Media Platform Preplay API.
* If [assetIds] have at least one value this property is ignored and could be empty
Expand All @@ -69,6 +75,7 @@ data class UplynkSsaiDescription(
fun contentProtected(contentProtected: Boolean) = apply { this.contentProtected = contentProtected }

private var preplayParameters: LinkedHashMap<String, String> = LinkedHashMap()

/**
* Sets the parameters.
*
Expand All @@ -81,13 +88,32 @@ data class UplynkSsaiDescription(
* linkedMapOf("ad" to "exampleAdServer")
* ```
*/
fun preplayParameters(parameters: LinkedHashMap<String, String>) = apply { this.preplayParameters = parameters }
fun preplayParameters(parameters: LinkedHashMap<String, String>) =
apply { this.preplayParameters = parameters }

private var assetInfo: Boolean = false

/**
* Sets flag to request asset info.
*/
fun assetInfo(shouldRequest: Boolean) = apply { this.assetInfo = shouldRequest }

private var assetType: UplynkAssetType = UplynkAssetType.ASSET

/**
* Sets the asset type.
*
* - For all possibilities, see {@link UplynkAssetType}.
*
* @param value The Verizon Media asset type. (<b>NonNull</b>)
hovig-theo marked this conversation as resolved.
Show resolved Hide resolved
*
*/
fun assetType(value: UplynkAssetType) = apply { this.assetType = value }

private var pingConfiguration: UplynkPingConfiguration = UplynkPingConfiguration()

fun pingConfiguration(value: UplynkPingConfiguration) = apply { this.pingConfiguration = value }

/**
* Builds the [UplynkSsaiDescription].
*/
Expand All @@ -98,6 +124,73 @@ data class UplynkSsaiDescription(
userId = userId,
contentProtected = contentProtected,
preplayParameters = preplayParameters,
assetInfo = assetInfo)
assetInfo = assetInfo,
assetType = assetType,
pingConfiguration = pingConfiguration
)
}
}

/**
* Describes the configuration of Verizon Media Ping features.
*
*/
@Serializable
data class UplynkPingConfiguration(
/**
* Whether to increase the accuracy of ad events by passing the current playback time in Ping requests.
*
* @remark Only available when {@link UplynkSsaiDescription.assetType} is `'asset'`.
*
* @defaultValue `false`
*
*/
val adImpressions: Boolean = false,

/**
* Whether to enable FreeWheel's Video View by Callback feature to send content impressions to the FreeWheel server.
*
* @remarks Only available when {@link UplynkSsaiDescription.assetType} is `'asset'`.
*
* @defaultValue `false`
*/
val freeWheelVideoViews: Boolean = false,

/**
* Whether to request information about upcoming ad breaks in the Ping responses.
*
* @defaultValue false.
*/
val linearAdData: Boolean = false) {
class Builder {
private var adImpressions: Boolean = false
fun adImpressions(value: Boolean) = apply { adImpressions = value }
private var freeWheelVideoViews: Boolean = false
fun freeWheelVideoViews(value: Boolean) = apply { freeWheelVideoViews = value }
private var linearAdData: Boolean = false
fun linearAdData(value: Boolean) = apply { linearAdData = value }
hovig-theo marked this conversation as resolved.
Show resolved Hide resolved

fun build() = UplynkPingConfiguration(
adImpressions = adImpressions,
freeWheelVideoViews = freeWheelVideoViews,
linearAdData = linearAdData
)
}
}

enum class UplynkAssetType {
/**
* A Video-on-demand content asset.
*/
ASSET,

/**
* A Live content channel.
*/
CHANNEL,

/**
* A Live event.
*/
EVENT;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.theoplayer.android.connector.uplynk.internal

import com.theoplayer.android.connector.uplynk.internal.network.UplynkApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

internal class PingScheduler(
private val uplynkApi: UplynkApi,
private val uplynkDescriptionConverter: UplynkSsaiDescriptionConverter,
private val prefix: String,
private val sessionId: String,
private val eventDispatcher: UplynkEventDispatcher,
private val adScheduler: UplynkAdScheduler
) {
private val NEGATIVE_TIME = (-1).toDuration(DurationUnit.SECONDS)

private var nextRequestTime: Duration = NEGATIVE_TIME
private var seekStart: Duration = NEGATIVE_TIME
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)

fun onTimeUpdate(time: Duration) {
if (nextRequestTime.isPositive() && time > nextRequestTime) {
nextRequestTime = NEGATIVE_TIME
performPing(uplynkDescriptionConverter.buildPingUrl(prefix, sessionId, time))
}
}

fun onStart() =
performPing(uplynkDescriptionConverter.buildStartPingUrl(prefix, sessionId, Duration.ZERO))


fun onSeeking(time: Duration) {
if (seekStart.isNegative()) {
seekStart = time
}
}

fun onSeeked(time: Duration) {
performPing(
uplynkDescriptionConverter.buildSeekedPingUrl(
prefix,
sessionId,
time,
seekStart
)
)
seekStart = NEGATIVE_TIME
}

fun destroy() {
job.cancel()
}

private fun performPing(url: String) = scope.launch {
val pingResponse = uplynkApi.ping(url)
nextRequestTime = pingResponse.nextTime
eventDispatcher.dispatchPingEvent(pingResponse)
if (pingResponse.ads != null) {
adScheduler.add(pingResponse.ads)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import com.theoplayer.android.api.player.Player
import com.theoplayer.android.api.source.SourceDescription
import com.theoplayer.android.api.source.drm.DRMConfiguration
import com.theoplayer.android.api.source.drm.KeySystemConfiguration
import com.theoplayer.android.connector.uplynk.UplynkAssetType
import com.theoplayer.android.connector.uplynk.UplynkSsaiDescription
import com.theoplayer.android.connector.uplynk.internal.network.PreplayInternalLiveResponse
import com.theoplayer.android.connector.uplynk.internal.network.PreplayInternalResponse
import com.theoplayer.android.connector.uplynk.internal.network.UplynkApi
import kotlin.time.DurationUnit
import kotlin.time.toDuration
Expand All @@ -21,29 +24,44 @@ internal class UplynkAdIntegration(
private val uplynkDescriptionConverter: UplynkSsaiDescriptionConverter,
private val uplynkApi: UplynkApi
) : ServerSideAdIntegrationHandler {
private var pingScheduler: PingScheduler? = null
private var adScheduler: UplynkAdScheduler? = null
private val player: Player
get() = theoplayerView.player

init {
player.addEventListener(PlayerEventTypes.TIMEUPDATE) {
adScheduler?.onTimeUpdate(it.currentTime.toDuration(DurationUnit.SECONDS))
val time = it.currentTime.toDuration(DurationUnit.SECONDS)
adScheduler?.onTimeUpdate(time)
pingScheduler?.onTimeUpdate(time)
}

player.addEventListener(PlayerEventTypes.SEEKING) {
pingScheduler?.onSeeking(it.currentTime.toDuration(DurationUnit.SECONDS))
}

player.addEventListener(PlayerEventTypes.SEEKED) {
pingScheduler?.onSeeked(it.currentTime.toDuration(DurationUnit.SECONDS))
}
}

override suspend fun resetSource() {
adScheduler = null
pingScheduler?.destroy()
}

override suspend fun setSource(source: SourceDescription): SourceDescription {
adScheduler = null
pingScheduler?.destroy()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have the resetSource() implemented, isn't these 2 lines redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetSource() is only called when we set the source to null. Not sure wether it was intended or it's a bug in the THEOplayer SSAI API.

This is why it's duplicated here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, didn't realize that.. I expect reset gets called whenever a new source is set regardless if it's a null or not.
Let's discuss internally for the THEOplayer SSAI API.


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

val preplayUrl = uplynkDescriptionConverter.buildPreplayUrl(ssaiDescription)
val internalResponse = uplynkApi.preplay(preplayUrl)
val minimalResponse = internalResponse.parseMinimalResponse()
val minimalResponse = if (ssaiDescription.assetType == UplynkAssetType.ASSET) {
requestVod(ssaiDescription).parseMinimalResponse()
} else {
requestLive(ssaiDescription).parseMinimalResponse()
}

var newUplynkSource = uplynkSource.replaceSrc(minimalResponse.playURL)

Expand All @@ -62,14 +80,15 @@ internal class UplynkAdIntegration(
add(0, newUplynkSource)
})

try {
val externalResponse = internalResponse.parseExternalResponse()
eventDispatcher.dispatchPreplayEvents(externalResponse)
adScheduler = UplynkAdScheduler(externalResponse.ads.breaks, AdHandler(controller))
} catch (e: Exception) {
eventDispatcher.dispatchPreplayFailure(e)
controller.error(e)
}
pingScheduler = PingScheduler(
uplynkApi,
uplynkDescriptionConverter,
minimalResponse.prefix,
minimalResponse.sid,
eventDispatcher,
adScheduler!!
)
pingScheduler?.onStart()
hovig-theo marked this conversation as resolved.
Show resolved Hide resolved

if (ssaiDescription.assetInfo) {
uplynkDescriptionConverter
Expand All @@ -88,4 +107,36 @@ internal class UplynkAdIntegration(

return newSource
}

private suspend fun requestLive(ssaiDescription: UplynkSsaiDescription): PreplayInternalLiveResponse {
return uplynkDescriptionConverter
.buildPreplayLiveUrl(ssaiDescription)
.let { uplynkApi.preplayLive(it) }
.also {
try {
val response = it.parseExternalResponse()
eventDispatcher.dispatchPreplayLiveEvents(response)
adScheduler = UplynkAdScheduler(listOf(), AdHandler(controller))
} catch (e: Exception) {
eventDispatcher.dispatchPreplayFailure(e)
controller.error(e)
}
}
}

private suspend fun requestVod(ssaiDescription: UplynkSsaiDescription): PreplayInternalResponse {
return uplynkDescriptionConverter
.buildPreplayVodUrl(ssaiDescription)
.let { uplynkApi.preplayVod(it) }
.also {
try {
val response = it.parseExternalResponse()
eventDispatcher.dispatchPreplayEvents(response)
adScheduler = UplynkAdScheduler(response.ads.breaks, AdHandler(controller))
} catch (e: Exception) {
eventDispatcher.dispatchPreplayFailure(e)
controller.error(e)
}
}
}
}
Loading