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 3088ff9a..ff468dcb 100644 --- a/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/connector/MainActivity.kt @@ -26,7 +26,9 @@ 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.uplynk.network.PingResponse +import com.theoplayer.android.connector.uplynk.network.PreplayLiveResponse +import com.theoplayer.android.connector.uplynk.network.PreplayVodResponse import com.theoplayer.android.connector.yospace.YospaceConnector const val TAG = "MainActivity" @@ -152,8 +154,12 @@ 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 onPreplayVodResponse(response: PreplayVodResponse) { + Log.d("UplynkConnectorEvents", "PREPLAY_VOD_RESPONSE $response") + } + + override fun onPreplayLiveResponse(response: PreplayLiveResponse) { + Log.d("UplynkConnectorEvents", "PREPLAY_LIVE_RESPONSE $response") } override fun onAssetInfoResponse(response: AssetInfoResponse) { @@ -168,6 +174,9 @@ class MainActivity : AppCompatActivity() { Log.d("UplynkConnectorEvents", "ASSET_INFO_RESPONSE Failure $exception") } + override fun onPingResponse(pingResponse: PingResponse) { + Log.d("UplynkConnectorEvents", "PING_RESPONSE $pingResponse") + } }) theoplayerView.player.ads.addEventListener(AdsEventTypes.AD_ERROR) { 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 16941287..03ab263b 100644 --- a/app/src/main/java/com/theoplayer/android/connector/Sources.kt +++ b/app/src/main/java/com/theoplayer/android/connector/Sources.kt @@ -5,6 +5,8 @@ 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.uplynk.UplynkAssetType +import com.theoplayer.android.connector.uplynk.UplynkPingConfiguration import com.theoplayer.android.connector.uplynk.UplynkSsaiDescription import com.theoplayer.android.connector.yospace.YospaceSsaiDescription import com.theoplayer.android.connector.yospace.YospaceStreamType @@ -111,20 +113,58 @@ val sources: List by lazy { .Builder() .prefix("https://content.uplynk.com") .assetInfo(true) - .assetIds(listOf( - "41afc04d34ad4cbd855db52402ef210e", - "c6b61470c27d44c4842346980ec2c7bd", - "588f9d967643409580aa5dbe136697a1", - "b1927a5d5bd9404c85fde75c307c63ad", - "7e9932d922e2459bac1599938f12b272", - "a4c40e2a8d5b46338b09d7f863049675", - "bcf7d78c4ff94c969b2668a6edc64278", - )) - .preplayParameters(linkedMapOf( - "ad" to "adtest", - "ad.lib" to "15_sec_spots" - )) - .build()) + .assetIds( + listOf( + "41afc04d34ad4cbd855db52402ef210e", + "c6b61470c27d44c4842346980ec2c7bd", + "588f9d967643409580aa5dbe136697a1", + "b1927a5d5bd9404c85fde75c307c63ad", + "7e9932d922e2459bac1599938f12b272", + "a4c40e2a8d5b46338b09d7f863049675", + "bcf7d78c4ff94c969b2668a6edc64278", + ) + ) + .preplayParameters( + linkedMapOf( + "ad" to "adtest", + "ad.lib" to "15_sec_spots" + ) + ) + .build() + ) + .build() + ) + .build() + ), + Source( + name = "Uplynk Live", + sourceDescription = SourceDescription + .Builder( + TypedSource.Builder("no source") + .ssai( + UplynkSsaiDescription + .Builder() + .prefix("https://content.uplynk.com") + .assetInfo(false) + .assetType(UplynkAssetType.CHANNEL) + .assetIds( + listOf( + "3c367669a83b4cdab20cceefac253684", + ) + ) + .preplayParameters( + linkedMapOf( + "ad" to "cleardashnew", + ) + ) + .pingConfiguration( + UplynkPingConfiguration.Builder() + .linearAdData(true) + .adImpressions(false) + .freeWheelVideoViews(false) + .build()) + .build() + ) .build() ) .build() diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkAdIntegration.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkAdIntegration.kt deleted file mode 100644 index e69de29b..00000000 diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkListener.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkListener.kt index 3346ec66..a1af5378 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkListener.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkListener.kt @@ -1,7 +1,9 @@ package com.theoplayer.android.connector.uplynk +import com.theoplayer.android.connector.uplynk.network.PingResponse import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse -import com.theoplayer.android.connector.uplynk.network.PreplayResponse +import com.theoplayer.android.connector.uplynk.network.PreplayLiveResponse +import com.theoplayer.android.connector.uplynk.network.PreplayVodResponse /** * A listener interface for receiving events related to Uplynk @@ -14,9 +16,18 @@ interface UplynkListener { * * 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. + * @param response the `PreplayVodResponse` object containing information relevant to the preplay request. */ - fun onPreplayResponse(response: PreplayResponse) {} + fun onPreplayVodResponse(response: PreplayVodResponse) {} + + /** + * 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 @@ -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 pingResponse the `PingResponse` object containing ping request result + */ + fun onPingResponse(pingResponse: PingResponse) {} } diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkSsaiDescription.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkSsaiDescription.kt index c355a257..c972101d 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkSsaiDescription.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/UplynkSsaiDescription.kt @@ -8,14 +8,60 @@ import kotlinx.serialization.Serializable */ @Serializable data class UplynkSsaiDescription( + /** + * Sets the prefix to use for Uplynk Media Platform Preplay API and Asset Info API requests. + * + * - If no prefix is set the default origin is used: https://content.uplynk.com + */ val prefix: String? = null, + + /** + * Sets a list of asset IDs for Uplynk Media Platform Preplay API. + */ val assetIds: List = listOf(), - val externalId: List = listOf(), + + /** + * 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 + */ + val externalIds: List = listOf(), + + /** + * 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 + */ val userId: String? = null, + + /** + * Sets whether the assets of the source are content protected. + */ val contentProtected: Boolean = false, + + /** + * Sets the parameters. + * + * - Each entry of the map contains the parameter name with associated value. + * - The parameters keep their order as it's maintained by LinkedHashMap. + */ val preplayParameters: LinkedHashMap = linkedMapOf(), - val assetInfo: Boolean = false -): CustomSsaiDescription() { + + /** + * Sets flag to request asset info. + */ + val assetInfo: Boolean = false, + + /** + * Sets the asset type. + * + * - For all possibilities, see {@link UplynkAssetType}. + */ + val assetType: UplynkAssetType = UplynkAssetType.ASSET, + + /** + * Sets the ping request configuration + */ + val pingConfiguration: UplynkPingConfiguration = UplynkPingConfiguration() +) : CustomSsaiDescription() { override val customIntegration: String get() = UplynkConnector.INTEGRATION_ID @@ -25,6 +71,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. * @@ -35,6 +82,7 @@ data class UplynkSsaiDescription( fun prefix(prefix: String) = apply { this.prefix = prefix } private var assetIds = emptyList() + /** * Sets a list of asset IDs for Uplynk Media Platform Preplay API. * @@ -43,6 +91,7 @@ data class UplynkSsaiDescription( fun assetIds(ids: List) = apply { this.assetIds = ids } private var externalIds: List = emptyList() + /** * 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 @@ -52,6 +101,7 @@ data class UplynkSsaiDescription( fun externalIds(ids: List) = 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 @@ -69,6 +119,7 @@ data class UplynkSsaiDescription( fun contentProtected(contentProtected: Boolean) = apply { this.contentProtected = contentProtected } private var preplayParameters: LinkedHashMap = LinkedHashMap() + /** * Sets the parameters. * @@ -81,23 +132,135 @@ data class UplynkSsaiDescription( * linkedMapOf("ad" to "exampleAdServer") * ``` */ - fun preplayParameters(parameters: LinkedHashMap) = apply { this.preplayParameters = parameters } + fun preplayParameters(parameters: LinkedHashMap) = + 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 Uplynk Media asset type. (NonNull) + * + */ + fun assetType(value: UplynkAssetType) = apply { this.assetType = value } + + private var pingConfiguration: UplynkPingConfiguration = UplynkPingConfiguration() + + /** + * Sets the ping request configuration + */ + fun pingConfiguration(value: UplynkPingConfiguration) = apply { this.pingConfiguration = value } + /** * Builds the [UplynkSsaiDescription]. */ fun build() = UplynkSsaiDescription( prefix = prefix, assetIds = assetIds, - externalId = externalIds, + externalIds = externalIds, 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 + /** + * 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` + * + */ + fun adImpressions(value: Boolean) = apply { adImpressions = value } + + private var freeWheelVideoViews: 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` + */ + fun freeWheelVideoViews(value: Boolean) = apply { freeWheelVideoViews = value } + + private var linearAdData: Boolean = false + /** + * Whether to request information about upcoming ad breaks in the Ping responses. + * + * @defaultValue false. + */ + fun linearAdData(value: Boolean) = apply { linearAdData = value } + + 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; +} diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/PingScheduler.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/PingScheduler.kt new file mode 100644 index 00000000..adfccaac --- /dev/null +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/PingScheduler.kt @@ -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(time: Duration) = + performPing(uplynkDescriptionConverter.buildStartPingUrl(prefix, sessionId, time)) + + + 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) + } + } +} diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdIntegration.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdIntegration.kt index 328214aa..9fe5b69c 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdIntegration.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdIntegration.kt @@ -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.PreplayInternalVodResponse import com.theoplayer.android.connector.uplynk.internal.network.UplynkApi import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -21,29 +24,48 @@ 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)) + } + + player.addEventListener(PlayerEventTypes.PLAY) { + pingScheduler?.onStart(it.currentTime.toDuration(DurationUnit.SECONDS)) } } override suspend fun resetSource() { adScheduler = null + pingScheduler?.destroy() } override suspend fun setSource(source: SourceDescription): SourceDescription { adScheduler = null + pingScheduler?.destroy() 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) @@ -62,13 +84,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) + if (UplynkPingFeatures.from(ssaiDescription) != UplynkPingFeatures.NO_PING) { + pingScheduler = PingScheduler( + uplynkApi, + uplynkDescriptionConverter, + minimalResponse.prefix, + minimalResponse.sid, + eventDispatcher, + adScheduler!! + ) } if (ssaiDescription.assetInfo) { @@ -88,4 +112,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): PreplayInternalVodResponse { + 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) + } + } + } } diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdScheduler.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdScheduler.kt index 500d4e74..9939ae13 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdScheduler.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkAdScheduler.kt @@ -2,6 +2,8 @@ package com.theoplayer.android.connector.uplynk.internal import com.theoplayer.android.connector.uplynk.network.UplynkAd import com.theoplayer.android.connector.uplynk.network.UplynkAdBreak +import com.theoplayer.android.connector.uplynk.network.UplynkAds +import java.util.concurrent.CopyOnWriteArrayList import kotlin.time.Duration data class UplynkAdBreakState( @@ -31,10 +33,10 @@ internal class UplynkAdScheduler( uplynkAdBreaks: List, private val adHandler: AdHandler ) { - private val adBreaks = uplynkAdBreaks.map { + private val adBreaks = CopyOnWriteArrayList(uplynkAdBreaks.map { adHandler.createAdBreak(it) UplynkAdBreakState(it, AdBreakState.NOT_PLAYED) - } + }) private fun moveToState( currentAdBreak: UplynkAdBreakState, @@ -128,4 +130,8 @@ internal class UplynkAdScheduler( return null } + fun add(ads: UplynkAds) = ads.breaks.forEach { + adHandler.createAdBreak(it) + adBreaks.add(UplynkAdBreakState(it, AdBreakState.NOT_PLAYED)) + } } \ No newline at end of file diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkEventDispatcher.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkEventDispatcher.kt index 4cb50e48..c0e35a8d 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkEventDispatcher.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkEventDispatcher.kt @@ -3,8 +3,10 @@ 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.PingResponse import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse -import com.theoplayer.android.connector.uplynk.network.PreplayResponse +import com.theoplayer.android.connector.uplynk.network.PreplayLiveResponse +import com.theoplayer.android.connector.uplynk.network.PreplayVodResponse import java.util.concurrent.CopyOnWriteArrayList internal class UplynkEventDispatcher { @@ -12,8 +14,12 @@ internal class UplynkEventDispatcher { private val listeners = CopyOnWriteArrayList() - fun dispatchPreplayEvents(response: PreplayResponse) = handler.post { - listeners.forEach { it.onPreplayResponse(response) } + fun dispatchPreplayEvents(response: PreplayVodResponse) = handler.post { + listeners.forEach { it.onPreplayVodResponse(response) } + } + + fun dispatchPreplayLiveEvents(response: PreplayLiveResponse) = handler.post { + listeners.forEach { it.onPreplayLiveResponse(response) } } fun dispatchAssetInfoEvents(assetInfo: AssetInfoResponse) = handler.post { @@ -28,6 +34,10 @@ internal class UplynkEventDispatcher { listeners.forEach { it.onPreplayFailure(e) } } + fun dispatchPingEvent(pingResponse: PingResponse) = handler.post { + listeners.forEach { it.onPingResponse(pingResponse) } + } + fun addListener(listener: UplynkListener) = listeners.add(listener) fun removeListener(listener: UplynkListener) = listeners.remove(listener) diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkPingFeatures.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkPingFeatures.kt new file mode 100644 index 00000000..c5504c54 --- /dev/null +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkPingFeatures.kt @@ -0,0 +1,28 @@ +package com.theoplayer.android.connector.uplynk.internal + +import com.theoplayer.android.connector.uplynk.UplynkAssetType +import com.theoplayer.android.connector.uplynk.UplynkSsaiDescription + +internal enum class UplynkPingFeatures(val pingfValue: Int) { + NO_PING(0), + AD_IMPRESSIONS(1), + FW_VIDEO_VIEWS(2), + AD_IMPRESSIONS_AND_FW_VIDEO_VIEWS(3), + LINEAR_AD_DATA(4); + + companion object { + fun from(ssaiDescription: UplynkSsaiDescription): UplynkPingFeatures { + val isVod = ssaiDescription.assetType == UplynkAssetType.ASSET + with(ssaiDescription.pingConfiguration) { + return when { + isVod && adImpressions && freeWheelVideoViews -> AD_IMPRESSIONS_AND_FW_VIDEO_VIEWS + isVod && adImpressions -> AD_IMPRESSIONS + isVod && freeWheelVideoViews -> FW_VIDEO_VIEWS + !isVod && linearAdData -> LINEAR_AD_DATA + else -> NO_PING + } + } + } + } + +} \ No newline at end of file diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverter.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverter.kt index ff647add..87081742 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverter.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverter.kt @@ -1,31 +1,68 @@ package com.theoplayer.android.connector.uplynk.internal +import com.theoplayer.android.connector.uplynk.UplynkAssetType import com.theoplayer.android.connector.uplynk.UplynkSsaiDescription +import kotlin.time.Duration internal class UplynkSsaiDescriptionConverter { private val DEFAULT_PREFIX = "https://content.uplynk.com" - fun buildPreplayUrl(ssaiDescription: UplynkSsaiDescription): String = with(ssaiDescription) { + fun buildPreplayVodUrl(ssaiDescription: UplynkSsaiDescription): String = with(ssaiDescription) { val prefix = prefix ?: DEFAULT_PREFIX - val assetIds = when { - assetIds.isEmpty() && externalId.size == 1 -> "$userId/${externalId.first()}.json" - assetIds.isEmpty() && externalId.size > 1 -> "$userId/${externalId.joinToString(",")}/multiple.json" - assetIds.size == 1 -> "${assetIds.first()}.json" - else -> assetIds.joinToString(separator = ",") + "/multiple.json" + + var url = "$prefix/preplay/$urlAssetId?v=2" + if (ssaiDescription.contentProtected) { + url += "&manifest=mpd" + url += "&rmt=wv" } - var url = "$prefix/preplay/$assetIds?v=2" + url += "&$pingParameters&$urlParameters" + + return url + } + + fun buildPreplayLiveUrl(ssaiDescription: UplynkSsaiDescription): String = with(ssaiDescription) { + val prefix = prefix ?: DEFAULT_PREFIX + + var url = "$prefix/preplay/$urlAssetType/$urlAssetId?v=2" if (ssaiDescription.contentProtected) { url += "&manifest=mpd" url += "&rmt=wv" } - val parameters = preplayParameters.map { "${it.key}=${it.value}" }.joinToString("&") - url += "&$parameters" + url += "&$pingParameters&$urlParameters" return url } + private val UplynkSsaiDescription.urlParameters + get() = preplayParameters.map { "${it.key}=${it.value}" }.joinToString("&") + + private val UplynkSsaiDescription.pingParameters: String + get() { + val feature = UplynkPingFeatures.from(this) + return if (feature == UplynkPingFeatures.NO_PING) { + "ad.pingc=0" + } else { + "ad.pingc=1&ad.pingf=${feature.pingfValue}" + } + } + + private val UplynkSsaiDescription.urlAssetType + get() = when (assetType) { + UplynkAssetType.ASSET -> "" + UplynkAssetType.CHANNEL -> "channel" + UplynkAssetType.EVENT -> "event" + } + + private val UplynkSsaiDescription.urlAssetId + get() = when { + assetIds.isEmpty() && externalIds.size == 1 -> "$userId/${externalIds.first()}.json" + assetIds.isEmpty() && externalIds.size > 1 -> "$userId/${externalIds.joinToString(",")}/multiple.json" + assetIds.size == 1 -> "${assetIds.first()}.json" + else -> assetIds.joinToString(separator = ",") + "/multiple.json" + } + fun buildAssetInfoUrls( ssaiDescription: UplynkSsaiDescription, sessionId: String, @@ -36,7 +73,7 @@ internal class UplynkSsaiDescriptionConverter { "$prefix/player/assetinfo/$it.json" } - externalId.isNotEmpty() -> externalId.map { + externalIds.isNotEmpty() -> externalIds.map { "$prefix/player/assetinfo/ext/$userId/$it.json" } @@ -48,4 +85,16 @@ internal class UplynkSsaiDescriptionConverter { urlList.map { "$it?pbs=$sessionId" } } } + + fun buildSeekedPingUrl( + prefix: String, sessionId: String, currentTime: Duration, seekStartTime: Duration + ) = buildPingUrl(prefix, sessionId, currentTime) + "&ev=seek&ft=${seekStartTime.inWholeSeconds}" + + fun buildStartPingUrl( + prefix: String, sessionId: String, currentTime: Duration + ) = buildPingUrl(prefix, sessionId, currentTime) + "&ev=start" + + fun buildPingUrl( + prefix: String, sessionId: String, currentTime: Duration + ) = "$prefix/session/ping/$sessionId.json?v=3&pt=${currentTime.inWholeSeconds}" } diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/PreplayInternalResponse.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/PreplayInternalVodResponse.kt similarity index 56% rename from connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/PreplayInternalResponse.kt rename to connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/PreplayInternalVodResponse.kt index c15d070f..d50135de 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/PreplayInternalResponse.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/PreplayInternalVodResponse.kt @@ -1,7 +1,8 @@ package com.theoplayer.android.connector.uplynk.internal.network import com.theoplayer.android.connector.uplynk.network.DrmResponse -import com.theoplayer.android.connector.uplynk.network.PreplayResponse +import com.theoplayer.android.connector.uplynk.network.PreplayLiveResponse +import com.theoplayer.android.connector.uplynk.network.PreplayVodResponse import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -31,7 +32,12 @@ internal data class MinimalPreplayResponse( ) -internal class PreplayInternalResponse(val body: String, private val json: Json) { +internal class PreplayInternalVodResponse(val body: String, private val json: Json) { fun parseMinimalResponse(): MinimalPreplayResponse = json.decodeFromString(body) - fun parseExternalResponse(): PreplayResponse = json.decodeFromString(body) + fun parseExternalResponse(): PreplayVodResponse = json.decodeFromString(body) +} + +internal class PreplayInternalLiveResponse(val body: String, private val json: Json) { + fun parseMinimalResponse(): MinimalPreplayResponse = json.decodeFromString(body) + fun parseExternalResponse(): PreplayLiveResponse = json.decodeFromString(body) } \ No newline at end of file diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/UplynkApi.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/UplynkApi.kt index 073aea52..636cc4d4 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/UplynkApi.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/internal/network/UplynkApi.kt @@ -1,6 +1,7 @@ package com.theoplayer.android.connector.uplynk.internal.network import com.theoplayer.android.connector.uplynk.network.AssetInfoResponse +import com.theoplayer.android.connector.uplynk.network.PingResponse import kotlinx.serialization.json.Json @@ -8,13 +9,23 @@ internal class UplynkApi { private val json = Json { ignoreUnknownKeys = true } private val network = HttpsConnection() - suspend fun preplay(srcURL: String): PreplayInternalResponse { + suspend fun preplayVod(srcURL: String): PreplayInternalVodResponse { val body = network.retry { get(srcURL) } - return PreplayInternalResponse(body, json) + return PreplayInternalVodResponse(body, json) + } + + suspend fun preplayLive(srcURL: String): PreplayInternalLiveResponse { + val body = network.retry { get(srcURL) } + return PreplayInternalLiveResponse(body, json) } suspend fun assetInfo(url: String): AssetInfoResponse { val body = network.retry { get(url) } return json.decodeFromString(body) } + + suspend fun ping(url: String): PingResponse { + val body = network.get(url) + return json.decodeFromString(body) + } } diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/AssetInfoResponse.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/AssetInfoResponse.kt index 0d519fd2..4c2b3328 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/AssetInfoResponse.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/AssetInfoResponse.kt @@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable * such as its ratings, duration, thumbnail information, and more. * * For further details, please refer to the Uplynk Documentation: - * [AssetInfo Documentation](https://docs.edgecast.com/video/index.html#Develop/AssetInfo.htm) + * [AssetInfo Documentation](https://docs.edgecast.com/video/#Develop/AssetInfo.htm) */ @Serializable data class AssetInfoResponse( @@ -38,8 +38,8 @@ data class AssetInfoResponse( * Returns whether an error occurred. * *
    - *
  • Zero if error - *
  • One otherwise. + *
  • One if error + *
  • Zero otherwise. *
*/ @SerialName("error") diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PingResponse.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PingResponse.kt new file mode 100644 index 00000000..48d82681 --- /dev/null +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PingResponse.kt @@ -0,0 +1,47 @@ +package com.theoplayer.android.connector.uplynk.network + +import com.theoplayer.android.connector.uplynk.internal.network.DurationToSecDeserializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlin.time.Duration + +/** + * The response for a ping request + * See more in [documentation](https://docs.edgecast.com/video/#Develop/Pingv2.htm) + */ +@Serializable +data class PingResponse( + /** + * Indicates the next playback position, in seconds, at which the player should request this endpoint. + * The player should not issue additional API requests when this parameter returns -1.0. + */ + @SerialName("next_time") + @Serializable(with = DurationToSecDeserializer::class) + val nextTime: Duration, + + /** + * Contains information about upcoming ads. + */ + val ads: UplynkAds? = null, + + /** + * **VAST Only** + * + * Contains the custom set of VAST extensions returned by the ad server. + * Each custom extension is reported as an object + * + * This is returned as a JsonElement because the extensions structure is custom. + * You could build deserialization logic if needed depending on the expected structure of this field + * + * Check more info in [documentation](https://docs.edgecast.com/video/#AdIntegration/VAST-VPAID.htm#CustomVASTExt) + */ + val extensions: JsonElement? = null, + + /** + * **Error Response Only** + * + * Describes the error that occurred. + */ + val error: String? = null +) diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayLiveResponse.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayLiveResponse.kt new file mode 100644 index 00000000..83ee08e7 --- /dev/null +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayLiveResponse.kt @@ -0,0 +1,49 @@ +package com.theoplayer.android.connector.uplynk.network + +import kotlinx.serialization.Serializable + +/** + * The Uplynk Preplay Response for live channels and events. + * + * For further details, please refer to the Uplynk Documentation: + * [Preplay API (Version 2) Documentation](https://docs.edgecast.com/video/#Develop/Preplayv2.htm) + */ +@Serializable +data class PreplayLiveResponse( + + /** + * The manifest's URL. (**NonNull**) + */ + val playURL: String, + + /** + * The identifier of the viewer's session. (**NonNull**) + */ + val sid: String, + + /* + * The content protection information. (**Nullable**) + */ + val drm: DrmResponse?, + + /** + * The zone prefix for the viewer's session. (**NonNull**) + * + * + * * Use this prefix when submitting playback or API requests for this session. + * + * + * + * Example: + * + * * Possible return value: 'https://content-ause2.uplynk.com/' + * + */ + val prefix: String, + + /** + * Contains a list of ads that took place during the time period defined by the ts and endts request parameters. + * + */ + val ads: List = listOf() +) diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayResponse.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayVodResponse.kt similarity index 75% rename from connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayResponse.kt rename to connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayVodResponse.kt index e7d62f45..8be4d4dc 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayResponse.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/PreplayVodResponse.kt @@ -3,13 +3,13 @@ package com.theoplayer.android.connector.uplynk.network import kotlinx.serialization.Serializable /** - * The Uplynk Preplay Response API. + * The Uplynk Preplay Response API for video on demand * * For further details, please refer to the Uplynk Documentation: * [Preplay API (Version 2) Documentation](https://docs.edgecast.com/video/#Develop/Preplayv2.htm) */ @Serializable -data class PreplayResponse( +data class PreplayVodResponse( /** * The manifest's URL. (**NonNull**) @@ -39,5 +39,10 @@ data class PreplayResponse( * Contains ad information, such as break offsets and non-video ads. (**NonNull**) * */ - val ads: UplynkAds -) + val ads: UplynkAds, + + /** + * Indicates the URL to the XML file containing interstitial information for Apple TV. + * This parameter reports null when ads are not found. + */ + val interstitialURL: String? = null) diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/UplynkAd.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/UplynkAd.kt index 1f2a93dd..c89c37f1 100644 --- a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/UplynkAd.kt +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/UplynkAd.kt @@ -2,6 +2,7 @@ package com.theoplayer.android.connector.uplynk.network import com.theoplayer.android.connector.uplynk.internal.network.DurationToSecDeserializer import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement import kotlin.time.Duration @@ -72,8 +73,13 @@ data class UplynkAd( /** * Contains the custom set of VAST extensions returned by the ad server. * Each custom extension is reported as an object. + * + * This is returned as a JsonElement because the extensions structure is custom. + * You could build deserialization logic if needed depending on the expected structure of this field + * + * Check more info in [documentation](https://docs.edgecast.com/video/#AdIntegration/VAST-VPAID.htm#CustomVASTExt) */ - val extensions: List>? = null, + val extensions: JsonElement? = null, /** * FreeWheel only: If the ad response provided by FreeWheel contains creative parameters, diff --git a/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/UplynkPlayedAd.kt b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/UplynkPlayedAd.kt new file mode 100644 index 00000000..20c71f5f --- /dev/null +++ b/connectors/uplynk/src/main/java/com/theoplayer/android/connector/uplynk/network/UplynkPlayedAd.kt @@ -0,0 +1,43 @@ +package com.theoplayer.android.connector.uplynk.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +/** + * A class that represents an advertisement that was played in live channel before playback started on the client + */ +@Serializable +data class UplynkPlayedAd ( + /** + * Indicates the duration, in seconds, of an ad break. + */ + val duration: Double, + + /** + * Indicates the duration, in seconds, of an ad break. + */ + val ts: Double, + + /** + * **VAST Only** + * + * Contains the custom set of VAST extensions returned by the ad server. + * + * This is returned as a JsonElement because the extensions structure is custom. + * You could build deserialization logic if needed depending on the expected structure of this field + * + * Check more info in [documentation](https://docs.edgecast.com/video/#AdIntegration/VAST-VPAID.htm#CustomVASTExt) + */ + val extensions: JsonElement? = null, + + /** + * **FreeWheel Only** + * If the ad response provided by FreeWheel contains creative parameters, + * they will be reported as name-value pairs within this object. + * + * Check more info in [documentation](https://docs.edgecast.com/video/#AdIntegration/Freewheel.htm) + */ + @SerialName("fw_parameters") + val freeWheelParameters: Map +) diff --git a/connectors/uplynk/src/test/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverterTest.kt b/connectors/uplynk/src/test/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverterTest.kt index 44265e5d..331032a3 100644 --- a/connectors/uplynk/src/test/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverterTest.kt +++ b/connectors/uplynk/src/test/java/com/theoplayer/android/connector/uplynk/internal/UplynkSsaiDescriptionConverterTest.kt @@ -26,7 +26,7 @@ class UplynkSsaiDescriptionConverterTest { @Test fun buildPreplayUrl_whenPrefixIsNotNull_startsUrlFromPrefix() { - val result = converter.buildPreplayUrl(ssaiDescription) + val result = converter.buildPreplayVodUrl(ssaiDescription) assertTrue(result.startsWith("preplayprefix")) } @@ -35,14 +35,14 @@ class UplynkSsaiDescriptionConverterTest { fun buildPreplayUrl_whenPrefixIsNull_startsUrlFromPrefix() { ssaiDescription = ssaiDescription.copy(prefix = null) - val result = converter.buildPreplayUrl(ssaiDescription) + val result = converter.buildPreplayVodUrl(ssaiDescription) assertTrue(result.startsWith("https://content.uplynk.com")) } @Test fun buildPreplayUrl_whenAssetIdHasMultipleValues_addsThemAsCommaSeparatedList() { - val result = converter.buildPreplayUrl(ssaiDescription) + val result = converter.buildPreplayVodUrl(ssaiDescription) assertTrue(result.contains("/asset1,asset2,asset3/")) } @@ -51,7 +51,7 @@ class UplynkSsaiDescriptionConverterTest { fun buildPreplayUrl_whenAssetIdHasSingleValue_usesItAsJsonFilename() { ssaiDescription = ssaiDescription.copy(assetIds = listOf("singleasset")) - val result = converter.buildPreplayUrl(ssaiDescription) + val result = converter.buildPreplayVodUrl(ssaiDescription) assertTrue(result.contains("/singleasset.json")) } @@ -59,10 +59,10 @@ class UplynkSsaiDescriptionConverterTest { @Test fun buildPreplayUrl_whenAssetIdsIsEmpty_addsUserIdAndExternalIds() { ssaiDescription = UplynkSsaiDescription( - assetIds = listOf(), externalId = listOf("extId1", "extId2"), userId = "userId" + assetIds = listOf(), externalIds = listOf("extId1", "extId2"), userId = "userId" ) - val result = converter.buildPreplayUrl(ssaiDescription) + val result = converter.buildPreplayVodUrl(ssaiDescription) assertTrue(result.contains("userId")) assertTrue(result.contains("extId1,extId2/multiple.json")) @@ -71,10 +71,10 @@ class UplynkSsaiDescriptionConverterTest { @Test fun buildPreplayUrl_whenAssetIdsIsEmptyAndExternalIdIsSingle_addsUserIdAndExternalId() { ssaiDescription = UplynkSsaiDescription( - assetIds = listOf(), externalId = listOf("extId1"), userId = "userId" + assetIds = listOf(), externalIds = listOf("extId1"), userId = "userId" ) - val result = converter.buildPreplayUrl(ssaiDescription) + val result = converter.buildPreplayVodUrl(ssaiDescription) assertTrue(result.contains("userId")) assertTrue(result.contains("extId1.json")) @@ -82,14 +82,14 @@ class UplynkSsaiDescriptionConverterTest { @Test fun buildPreplayUrl_always_followsTheTemplate() { - val result = converter.buildPreplayUrl(ssaiDescription) + val result = converter.buildPreplayVodUrl(ssaiDescription) val items = result.split("/", "?") assertEquals("preplayprefix", items[0]) assertEquals("preplay", items[1]) assertEquals("asset1,asset2,asset3", items[2]) assertEquals("multiple.json", items[3]) - assertEquals("v=2&p1=v1&p2=v2&p3=v3", items[4]) + assertEquals("v=2&ad.pingc=0&p1=v1&p2=v2&p3=v3", items[4]) } @Test @@ -109,7 +109,7 @@ class UplynkSsaiDescriptionConverterTest { @Test fun buildAssetInfoUrls_whenAssetIdIsEmptyAndExternalIdIsEmpty_returnsEmptyUrl() { ssaiDescription = UplynkSsaiDescription( - assetIds = listOf(), externalId = listOf() + assetIds = listOf(), externalIds = listOf() ) val result = converter.buildAssetInfoUrls(ssaiDescription, "", "prefix") @@ -132,7 +132,7 @@ class UplynkSsaiDescriptionConverterTest { fun buildAssetInfoUrls_whenAssetIdIsEmpty_returnsAssetInfoUrlsUsingExternalId() { ssaiDescription = ssaiDescription.copy( assetIds = listOf(), - externalId = listOf("extId1", "extId2"), + externalIds = listOf("extId1", "extId2"), userId = "userId" )