Skip to content

Commit 2b2aacb

Browse files
committed
fix: preload adaptive SABR initialization ranges
1 parent 6d49da2 commit 2b2aacb

4 files changed

Lines changed: 124 additions & 23 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dev.typetype.server.services
2+
3+
import dev.typetype.server.cache.CacheService
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.runInterruptible
6+
import org.schabi.newpipe.extractor.localization.Localization
7+
import org.schabi.newpipe.extractor.services.youtube.sabr.YoutubeSabrFormat
8+
9+
internal object SabrAdaptiveInitialization {
10+
private val localization = Localization("en", "US")
11+
12+
suspend fun fetch(
13+
holder: SabrSessionHolder,
14+
format: YoutubeSabrFormat,
15+
cache: CacheService?,
16+
timeoutMs: Long = 2_000L,
17+
): ByteArray? {
18+
SabrInitializationData.fetch(holder.key.videoId, format, cache)?.let {
19+
holder.session.streamState.ingestInitializationData(format, it)
20+
return it
21+
}
22+
val data = fetchRange(holder, format, timeoutMs) ?: return null
23+
SabrInitializationData.remember(holder.key.videoId, format, data, cache)
24+
return data
25+
}
26+
27+
suspend fun fetchRange(
28+
holder: SabrSessionHolder,
29+
format: YoutubeSabrFormat,
30+
timeoutMs: Long,
31+
): ByteArray? {
32+
val poToken = holder.session.streamState.poToken?.takeIf { it.isNotEmpty() }
33+
?: holder.playerContextToken?.streamingPoTokenBytesFor(holder.info)
34+
?: return null
35+
return runCatchingNonCancellation {
36+
runInterruptible(Dispatchers.IO) {
37+
holder.withPlayerContext {
38+
fetchInitializationData(format, localization, timeoutMs, poToken)
39+
}
40+
}
41+
}.getOrNull()
42+
}
43+
}

src/main/kotlin/dev/typetype/server/services/SabrDownloadInitialization.kt

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,15 @@
11
package dev.typetype.server.services
22

3-
import kotlinx.coroutines.Dispatchers
4-
import kotlinx.coroutines.runInterruptible
5-
import org.schabi.newpipe.extractor.localization.Localization
63
import org.schabi.newpipe.extractor.services.youtube.sabr.YoutubeSabrFormat
74

85
internal object SabrDownloadInitialization {
9-
private val localization = Localization("en", "US")
10-
116
suspend fun fetch(
127
store: SabrSessionStore,
138
holder: SabrSessionHolder,
149
format: YoutubeSabrFormat,
1510
): ByteArray? {
16-
SabrInitializationData.fetch(holder.key.videoId, format, store.initCache)?.let {
17-
holder.session.streamState.ingestInitializationData(format, it)
18-
return it
19-
}
20-
val poToken = holder.session.streamState.poToken?.takeIf { it.isNotEmpty() }
21-
?: holder.playerContextToken?.streamingPoTokenBytesFor(holder.info)
11+
val direct = SabrAdaptiveInitialization.fetch(holder, format, store.initCache, DIRECT_TIMEOUT_MS)
2212
?: return store.fetchInitializationData(holder, format)
23-
val direct = runCatchingNonCancellation {
24-
runInterruptible(Dispatchers.IO) {
25-
holder.withPlayerContext {
26-
fetchInitializationData(format, localization, DIRECT_TIMEOUT_MS, poToken)
27-
}
28-
}
29-
}.getOrNull() ?: return store.fetchInitializationData(holder, format)
30-
SabrInitializationData.remember(holder.key.videoId, format, direct, store.initCache)
3113
return direct
3214
}
3315

src/main/kotlin/dev/typetype/server/services/SabrSessionStore.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,7 @@ internal class SabrSessionStore(
184184
holder.liveInitialization(format)?.let { return it }
185185
val request = SabrSegmentRequest.initialization(format)
186186
holder.session.getCachedSegment(request)?.let { segmentCache.put(holder, it); return it.data }
187-
SabrInitializationData.fetch(holder.key.videoId, format, initCache)?.let {
188-
holder.session.streamState.ingestInitializationData(format, it)
189-
return it
190-
}
187+
SabrAdaptiveInitialization.fetch(holder, format, initCache)?.let { return it }
191188
SabrInitializationData.bootstrap(holder, format, initCache)?.let { return it }
192189
val segment = pump.fetchSegment(holder, request) ?: return null
193190
segmentCache.put(holder, segment)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package dev.typetype.server.services
2+
3+
import io.mockk.every
4+
import io.mockk.mockk
5+
import io.mockk.verify
6+
import kotlinx.coroutines.runBlocking
7+
import org.junit.jupiter.api.Assertions.assertArrayEquals
8+
import org.junit.jupiter.api.Assertions.assertNull
9+
import org.junit.jupiter.api.Test
10+
import org.schabi.newpipe.extractor.localization.Localization
11+
import org.schabi.newpipe.extractor.services.youtube.sabr.YoutubeSabrFormat
12+
import org.schabi.newpipe.extractor.services.youtube.sabr.YoutubeSabrInfo
13+
import org.schabi.newpipe.extractor.services.youtube.sabr.YoutubeSabrSession
14+
import org.schabi.newpipe.extractor.services.youtube.sabr.YoutubeSabrStreamState
15+
16+
class SabrAdaptiveInitializationTest {
17+
@Test
18+
fun `fetches initialization range with the active streaming token`() = runBlocking {
19+
val fixture = fixture(byteArrayOf(1, 2, 3))
20+
val expected = byteArrayOf(4, 5, 6)
21+
every {
22+
fixture.session.fetchInitializationData(
23+
fixture.format,
24+
any<Localization>(),
25+
2_000L,
26+
match { it.contentEquals(fixture.poToken) },
27+
)
28+
} returns expected
29+
30+
val actual = SabrAdaptiveInitialization.fetchRange(fixture.holder, fixture.format, 2_000L)
31+
32+
assertArrayEquals(expected, actual)
33+
}
34+
35+
@Test
36+
fun `returns null when the adaptive range request fails`() = runBlocking {
37+
val fixture = fixture(byteArrayOf(1))
38+
every {
39+
fixture.session.fetchInitializationData(
40+
fixture.format,
41+
any<Localization>(),
42+
2_000L,
43+
any(),
44+
)
45+
} throws java.io.IOException("range unavailable")
46+
47+
assertNull(SabrAdaptiveInitialization.fetchRange(fixture.holder, fixture.format, 2_000L))
48+
}
49+
50+
@Test
51+
fun `does not request a range without a streaming token`() = runBlocking {
52+
val fixture = fixture(null)
53+
54+
assertNull(SabrAdaptiveInitialization.fetchRange(fixture.holder, fixture.format, 2_000L))
55+
verify(exactly = 0) {
56+
fixture.session.fetchInitializationData(any(), any(), any(), any())
57+
}
58+
}
59+
60+
private fun fixture(poToken: ByteArray?): Fixture {
61+
val holder = mockk<SabrSessionHolder>()
62+
val session = mockk<YoutubeSabrSession>()
63+
val state = mockk<YoutubeSabrStreamState>()
64+
val format = mockk<YoutubeSabrFormat>()
65+
every { holder.session } returns session
66+
every { holder.playerContextToken } returns null
67+
every { holder.info } returns mockk<YoutubeSabrInfo>()
68+
every { session.streamState } returns state
69+
every { state.poToken } returns poToken
70+
return Fixture(holder, session, format, poToken ?: byteArrayOf())
71+
}
72+
73+
private data class Fixture(
74+
val holder: SabrSessionHolder,
75+
val session: YoutubeSabrSession,
76+
val format: YoutubeSabrFormat,
77+
val poToken: ByteArray,
78+
)
79+
}

0 commit comments

Comments
 (0)