Skip to content

Commit

Permalink
Merge pull request #34 from THEOplayer/maintenance/implement-retry
Browse files Browse the repository at this point in the history
Uplynk: implement retry logic
  • Loading branch information
OlegRyz authored Sep 4, 2024
2 parents 06fd35f + d3edb5c commit 896f4a8
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import kotlin.time.DurationUnit
private val Duration.secToMs: Int
get() = this.toInt(DurationUnit.MILLISECONDS)

class AdHandler(private val controller: ServerSideAdIntegrationController) {
@Suppress("UnstableApiUsage")
internal class AdHandler(private val controller: ServerSideAdIntegrationController) {
private val scheduledAds = WeakHashMap<UplynkAd, Ad>()

fun createAdBreak(adBreak: UplynkAdBreak) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import com.theoplayer.android.api.event.player.PlayerEventTypes
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.FairPlayKeySystemConfiguration
import com.theoplayer.android.api.source.drm.KeySystemConfiguration
import com.theoplayer.android.connector.uplynk.UplynkSsaiDescription
import com.theoplayer.android.connector.uplynk.internal.network.UplynkApi
import kotlin.time.DurationUnit
import kotlin.time.toDuration

@Suppress("UnstableApiUsage")
internal class UplynkAdIntegration(
private val theoplayerView: THEOplayerView,
private val controller: ServerSideAdIntegrationController,
Expand Down Expand Up @@ -73,7 +73,7 @@ internal class UplynkAdIntegration(

if (ssaiDescription.assetInfo) {
uplynkDescriptionConverter
.buildAssetInfoUrls(ssaiDescription, minimalResponse.sid)
.buildAssetInfoUrls(ssaiDescription, minimalResponse.sid, minimalResponse.prefix)
.mapNotNull {
try {
uplynkApi.assetInfo(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ enum class AdBreakState {
FINISHED,
}

class UplynkAdScheduler(
internal class UplynkAdScheduler(
uplynkAdBreaks: List<UplynkAdBreak>,
private val adHandler: AdHandler
) {
Expand Down Expand Up @@ -90,21 +90,18 @@ class UplynkAdScheduler(
private fun endAllStartedAds(
currentAdBreak: UplynkAdBreakState,
currentAd: UplynkAdState? = null
) {
currentAdBreak.ads
.takeWhile { currentAd == null || it != currentAd }
.forEach {
moveAdToState(it, AdState.COMPLETED)
}
}
) = currentAdBreak.ads
.filter { it.state == AdState.STARTED && it != currentAd }
.forEach {
moveAdToState(it, AdState.COMPLETED)
}

private fun endAllAdBreaks() = adBreaks
.filter { it.state == AdBreakState.STARTED }
.forEach { moveToState(it, AdBreakState.FINISHED) }

private fun endAllAdBreaksExcept(currentAdBreak: UplynkAdBreakState?) = adBreaks
.filter { it != currentAdBreak }
.filter { it.state == AdBreakState.STARTED }
.filter { it.state == AdBreakState.STARTED && it != currentAdBreak }
.forEach { moveToState(it, AdBreakState.FINISHED) }

private fun beginCurrentAdBreak(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ internal class UplynkSsaiDescriptionConverter {
return url
}

fun buildAssetInfoUrls(ssaiDescription: UplynkSsaiDescription, sessionId: String): List<String> = with(ssaiDescription) {
val prefix = prefix ?: DEFAULT_PREFIX
fun buildAssetInfoUrls(
ssaiDescription: UplynkSsaiDescription,
sessionId: String,
prefix: String
): List<String> = with(ssaiDescription) {
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()

else -> emptyList()
}
return if (sessionId.isBlank()) {
urlList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit

object DurationToSecDeserializer : KSerializer<Duration> {
internal object DurationToSecDeserializer : KSerializer<Duration> {

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("DurationSeconds", PrimitiveKind.DOUBLE)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.theoplayer.android.connector.uplynk.internal.network

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runInterruptible
import java.io.IOException
import java.net.URL
import javax.net.ssl.HttpsURLConnection
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

internal const val CONNECT_TIMEOUT_IN_MS = 30000
internal const val READ_TIMEOUT_IN_MS = 30000
Expand Down Expand Up @@ -36,4 +39,24 @@ internal class HttpsConnection {
connection?.disconnect()
}
}

suspend fun retry(
retry: Int = 3,
initialDelay: Duration = 300.milliseconds,
maxDelay: Duration = 5000.milliseconds,
delayMultiplier: Double = 2.0,
request: suspend HttpsConnection.() -> String
): String {
var delayDuration = initialDelay
for (remains in retry - 1 downTo 0) {
try {
return request()
} catch (e: IOException) {
if (remains == 0) throw IOException(e)
delay(delayDuration)
delayDuration = (delayDuration * delayMultiplier).coerceAtMost(maxDelay)
}
}
throw IllegalStateException("Retry count should be bigger than 0 but it is $retry")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ internal data class MinimalPreplayResponse(
*/
val sid: String,

/**
* The zone prefix for the viewer's session. (**NonNull**)
*/
val prefix: String,

/*
* The content protection information. (**Nullable**)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ internal class UplynkApi {
private val network = HttpsConnection()

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

suspend fun assetInfo(url: String): AssetInfoResponse {
val body = network.get(url)
val body = network.retry { get(url) }
return json.decodeFromString(body)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class UplynkSsaiDescriptionConverterTest {
fun setUp() {
MockitoAnnotations.openMocks(this)
ssaiDescription = UplynkSsaiDescription(
prefix = "urlprefix",
prefix = "preplayprefix",
assetIds = listOf("asset1", "asset2", "asset3"),
preplayParameters = LinkedHashMap(mapOf("p1" to "v1", "p2" to "v2", "p3" to "v3"))
)
Expand All @@ -28,7 +28,7 @@ class UplynkSsaiDescriptionConverterTest {
fun buildPreplayUrl_whenPrefixIsNotNull_startsUrlFromPrefix() {
val result = converter.buildPreplayUrl(ssaiDescription)

assertTrue(result.startsWith("urlprefix"))
assertTrue(result.startsWith("preplayprefix"))
}

@Test
Expand Down Expand Up @@ -85,7 +85,7 @@ class UplynkSsaiDescriptionConverterTest {
val result = converter.buildPreplayUrl(ssaiDescription)

val items = result.split("/", "?")
assertEquals("urlprefix", items[0])
assertEquals("preplayprefix", items[0])
assertEquals("preplay", items[1])
assertEquals("asset1,asset2,asset3", items[2])
assertEquals("multiple.json", items[3])
Expand All @@ -94,14 +94,14 @@ class UplynkSsaiDescriptionConverterTest {

@Test
fun buildAssetInfoUrls_withoutSid_doesNotContainPbsParameter() {
val result = converter.buildAssetInfoUrls(ssaiDescription, "")
val result = converter.buildAssetInfoUrls(ssaiDescription, "", "prefix")

assertTrue(result.none { it.contains("pbs=") })
}

@Test
fun buildAssetInfoUrls_withSid_hasPbsParameter() {
val result = converter.buildAssetInfoUrls(ssaiDescription, "sessionId")
val result = converter.buildAssetInfoUrls(ssaiDescription, "sessionId", "prefix")

assertTrue(result.all { it.contains("pbs=sessionId") })
}
Expand All @@ -112,20 +112,20 @@ class UplynkSsaiDescriptionConverterTest {
assetIds = listOf(), externalId = listOf()
)

val result = converter.buildAssetInfoUrls(ssaiDescription, "")
val result = converter.buildAssetInfoUrls(ssaiDescription, "", "prefix")

assertEquals(0, result.size)
}

@Test
fun buildAssetInfoUrls_whenAssetIdHasValues_returnsAssetInfoUrls() {
val result = converter.buildAssetInfoUrls(ssaiDescription, "")
val result = converter.buildAssetInfoUrls(ssaiDescription, "", "prefix")

assertEquals(3, result.size)

assertContains(result, "urlprefix/player/assetinfo/asset1.json")
assertContains(result, "urlprefix/player/assetinfo/asset2.json")
assertContains(result, "urlprefix/player/assetinfo/asset3.json")
assertContains(result, "prefix/player/assetinfo/asset1.json")
assertContains(result, "prefix/player/assetinfo/asset2.json")
assertContains(result, "prefix/player/assetinfo/asset3.json")
}

@Test
Expand All @@ -136,23 +136,11 @@ class UplynkSsaiDescriptionConverterTest {
userId = "userId"
)

val result = converter.buildAssetInfoUrls(ssaiDescription, "")
val result = converter.buildAssetInfoUrls(ssaiDescription, "", "prefix")

assertEquals(2, result.size)

assertContains(result, "urlprefix/player/assetinfo/ext/userId/extId1.json")
assertContains(result, "urlprefix/player/assetinfo/ext/userId/extId2.json")
}


@Test
fun buildAssetInfoUrls_whenPrefixIsNotSet_returnsUrlWithDefaultPrefix() {
ssaiDescription = UplynkSsaiDescription(
assetIds = listOf("assetId1"),
)

val result = converter.buildAssetInfoUrls(ssaiDescription, "")

assertEquals("https://content.uplynk.com/player/assetinfo/assetId1.json", result.first())
assertContains(result, "prefix/player/assetinfo/ext/userId/extId1.json")
assertContains(result, "prefix/player/assetinfo/ext/userId/extId2.json")
}
}

0 comments on commit 896f4a8

Please sign in to comment.