Skip to content

Commit b62ad2a

Browse files
committed
perf: reuse prewarmed SABR playback sessions
1 parent 092bd14 commit b62ad2a

6 files changed

Lines changed: 184 additions & 21 deletions

File tree

‎openapi.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ paths:
3232
/proxy: { $ref: ./openapi/paths/proxy.yaml#/Proxy }
3333
/media/{handle}: { $ref: ./openapi/paths/media.yaml#/ProviderMedia }
3434
/sabr/download/{videoId}: { $ref: ./openapi/paths/sabr-download.yaml#/SabrDownload }
35+
/sabr/playback/{videoId}: { $ref: ./openapi/paths/sabr-playback.yaml#/SabrPlaybackCreate }
3536
/sabr/playback/{sessionId}/position: { $ref: ./openapi/paths/sabr-playback.yaml#/SabrPlaybackPosition }
3637
/sabr/playback/{sessionId}/window: { $ref: ./openapi/paths/sabr-playback.yaml#/SabrPlaybackWindow }
3738
/sabr/playback/{sessionId}/prefetch: { $ref: ./openapi/paths/sabr-playback.yaml#/SabrPlaybackPrefetch }

‎openapi/components/sabr-playback.yaml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
SabrPlaybackCreateResponse:
2+
type: object
3+
required: [sessionId, videoId, videoItag, audioItag, startTimeMs, generation, ready, status]
4+
additionalProperties: true
5+
properties:
6+
sessionId: { type: string }
7+
videoId: { type: string }
8+
manifestUrl: { type: string, nullable: true }
9+
videoItag: { type: integer, minimum: 1 }
10+
audioItag: { type: integer, minimum: 1 }
11+
audioTrackId: { type: string, nullable: true }
12+
startTimeMs: { type: integer, format: int64, minimum: 0 }
13+
generation: { type: integer, format: int64, minimum: 0 }
14+
ready: { type: boolean }
15+
status: { type: string }
16+
retryAfterMs: { type: integer, format: int64, nullable: true }
17+
live: { type: object, nullable: true, additionalProperties: true }
118
SabrPlaybackBufferedRange:
219
type: object
320
required: [itag, startMs, endMs]

‎openapi/paths/sabr-playback.yaml‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,49 @@
1+
SabrPlaybackCreate:
2+
post:
3+
tags: [playback]
4+
summary: Create a SABR session or transfer a matching prewarmed session
5+
parameters:
6+
- name: videoId
7+
in: path
8+
required: true
9+
schema: { type: string }
10+
- name: videoItag
11+
in: query
12+
schema: { type: integer, minimum: 1 }
13+
- name: audioItag
14+
in: query
15+
schema: { type: integer, minimum: 1 }
16+
- name: audioTrackId
17+
in: query
18+
schema: { type: string }
19+
- name: startTimeMs
20+
in: query
21+
schema: { type: integer, format: int64, minimum: 0, default: 0 }
22+
- name: audioOnly
23+
in: query
24+
schema: { type: boolean, default: false }
25+
- name: isLive
26+
in: query
27+
schema: { type: boolean, default: false }
28+
- name: prewarm
29+
in: query
30+
description: Keep this session briefly for the next matching creation by the same account.
31+
schema: { type: boolean, default: false }
32+
responses:
33+
'200':
34+
description: Playback session created or transferred.
35+
content:
36+
application/json:
37+
schema:
38+
$ref: ../components/sabr-playback.yaml#/SabrPlaybackCreateResponse
39+
'202':
40+
description: Playback session is still preparing.
41+
content:
42+
application/json:
43+
schema:
44+
$ref: ../components/sabr-playback.yaml#/SabrPlaybackCreateResponse
45+
'422':
46+
$ref: ../components/common.yaml#/JsonError
147
SabrPlaybackPosition:
248
post:
349
tags: [playback]

‎server-http/src/main/kotlin/dev/typetype/server/routes/SabrPlaybackHandler.kt‎

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import dev.typetype.server.services.StreamService
1818
import dev.typetype.server.services.markServed
1919
import io.ktor.http.HttpStatusCode
2020
import io.ktor.server.application.ApplicationCall
21-
import io.ktor.server.request.receive
2221
import io.ktor.server.response.respond
2322
import dev.typetype.server.sabr.YoutubeSabrFormat
2423

@@ -34,12 +33,23 @@ internal class SabrPlaybackHandler(
3433
private val playbackService = SabrPlaybackSessionService(sabrSessionStore)
3534
private val infoResolver = SabrPlaybackInfoResolver(sabrSessionStore, authenticatedSabrInfoService)
3635
private val accessValidator = SabrPlaybackAccessValidator(streamService, youtubeSessionStreamInfo)
36+
private val prewarmHandoffs = SabrPlaybackPrewarmHandoffs()
3737

3838
suspend fun create(call: ApplicationCall, videoId: String) {
3939
val access = call.accessProfileOrRespond(authService, accessControlService, adminSettingsService) ?: return
4040
if (!validateAccess(call, videoId, access)) return
4141
val request = call.playbackRequest()
4242
val startTimeMs = request.effectiveStartTimeMs()
43+
val isPrewarm = call.request.queryParameters["prewarm"]?.toBooleanStrictOrNull() == true
44+
val handoffKey = prewarmHandoffs.key(access.userId, videoId, request, startTimeMs)
45+
if (!isPrewarm) {
46+
val handoff = handoffKey?.let(prewarmHandoffs::take)
47+
val existing = handoff?.let { playbackService.lookup(it.sessionToken) }
48+
if (existing != null && existing.terminalFailure() == null) {
49+
PlaybackTraceLog.record("sabr_prewarm_handoff", "result=hit videoItag=${existing.videoFormat.itag} audioItag=${existing.audioFormat.itag} audioTrackId=${existing.audioFormat.audioTrackId} startTimeMs=${handoff.startTimeMs} ready=${handoff.ready}")
50+
return respondPrepared(call, existing, videoId, handoff.startTimeMs, handoff.ready)
51+
}
52+
}
4353
val infoStartedAt = System.nanoTime()
4454
val prepared = infoResolver.initial(access.userId, videoId, startTimeMs)
4555
PlaybackTraceLog.record("sabr_info", "durationMs=${(System.nanoTime() - infoStartedAt) / 1_000_000} result=${if (prepared == null) "miss" else "ready"} startTimeMs=$startTimeMs")
@@ -59,6 +69,10 @@ internal class SabrPlaybackHandler(
5969
)
6070
PlaybackTraceLog.record("sabr_session_prepare", "durationMs=${(System.nanoTime() - prepareStartedAt) / 1_000_000} ready=${preparation.ready} videoItag=${video.itag} audioItag=${audio.itag} audioTrackId=${audio.audioTrackId} audioOnly=${request.audioOnly} isLive=${request.isLive} startTimeMs=${preparation.startTimeMs}")
6171
preparation.holder.setActiveTracks(videoActive = !request.audioOnly, audioActive = true)
72+
if (isPrewarm && handoffKey != null && preparation.holder.terminalFailure() == null) {
73+
prewarmHandoffs.remember(handoffKey, SabrPlaybackPrewarmSession(preparation.holder.sessionToken, preparation.startTimeMs, preparation.ready))
74+
PlaybackTraceLog.record("sabr_prewarm_handoff", "result=stored videoItag=${video.itag} audioItag=${audio.itag} audioTrackId=${audio.audioTrackId} startTimeMs=$startTimeMs")
75+
}
6276
respondPrepared(call, preparation.holder, videoId, preparation.startTimeMs, preparation.ready)
6377
}
6478

@@ -160,26 +174,6 @@ internal class SabrPlaybackHandler(
160174
}
161175
}
162176

163-
private suspend fun ApplicationCall.playbackRequest(): SabrPlaybackRequest {
164-
val body = runCatching { receive<SabrPlaybackRequest>() }.getOrNull()
165-
return SabrPlaybackRequest(
166-
videoItag = body?.videoItag ?: request.queryParameters["videoItag"]?.toIntOrNull(),
167-
audioItag = body?.audioItag ?: request.queryParameters["audioItag"]?.toIntOrNull(),
168-
audioTrackId = body?.audioTrackId ?: request.queryParameters["audioTrackId"],
169-
startTimeMs = body?.startTimeMs ?: request.queryParameters["startTimeMs"]?.toLongOrNull(),
170-
playerTimeMs = body?.playerTimeMs ?: request.queryParameters["playerTimeMs"]?.toLongOrNull(),
171-
audioOnly = body?.audioOnly ?: request.queryParameters["audioOnly"]?.toBooleanStrictOrNull() ?: false,
172-
isLive = body?.isLive ?: request.queryParameters["isLive"]?.toBooleanStrictOrNull() ?: false,
173-
)
174-
}
175-
176-
private fun SabrPlaybackRequest.effectiveStartTimeMs(): Long =
177-
(playerTimeMs ?: startTimeMs ?: 0L).coerceAtLeast(0L)
178-
179-
private fun SabrPlaybackRequest.keepsCurrentFormats(holder: SabrSessionHolder): Boolean =
180-
(videoItag == null || videoItag == holder.videoFormat.itag) &&
181-
(audioItag == null || audioItag == holder.audioFormat.itag) &&
182-
(audioTrackId.isNullOrBlank() || audioTrackId == holder.audioFormat.audioTrackId)
183177

184178
private suspend fun selectAudio(
185179
call: ApplicationCall,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package dev.typetype.server.routes
2+
3+
import java.util.LinkedHashMap
4+
5+
internal data class SabrPlaybackPrewarmKey(
6+
val userId: String,
7+
val videoId: String,
8+
val videoItag: Int,
9+
val audioItag: Int,
10+
val audioTrackId: String?,
11+
val startTimeMs: Long,
12+
val audioOnly: Boolean,
13+
val isLive: Boolean,
14+
)
15+
16+
internal data class SabrPlaybackPrewarmSession(
17+
val sessionToken: String,
18+
val startTimeMs: Long,
19+
val ready: Boolean,
20+
)
21+
22+
internal class SabrPlaybackPrewarmHandoffs(
23+
private val maxEntries: Int = MAX_ENTRIES,
24+
private val ttlNanos: Long = TTL_NANOS,
25+
) {
26+
private data class Entry(val session: SabrPlaybackPrewarmSession, val expiresAtNanos: Long)
27+
28+
private val entries = LinkedHashMap<SabrPlaybackPrewarmKey, Entry>()
29+
30+
fun key(
31+
userId: String?,
32+
videoId: String,
33+
request: SabrPlaybackRequest,
34+
startTimeMs: Long,
35+
): SabrPlaybackPrewarmKey? {
36+
val scope = userId?.takeIf { it.isNotBlank() } ?: return null
37+
val videoItag = request.videoItag ?: return null
38+
val audioItag = request.audioItag ?: return null
39+
return SabrPlaybackPrewarmKey(
40+
userId = scope,
41+
videoId = videoId,
42+
videoItag = videoItag,
43+
audioItag = audioItag,
44+
audioTrackId = request.audioTrackId,
45+
startTimeMs = startTimeMs,
46+
audioOnly = request.audioOnly,
47+
isLive = request.isLive,
48+
)
49+
}
50+
51+
@Synchronized
52+
fun remember(key: SabrPlaybackPrewarmKey, session: SabrPlaybackPrewarmSession) {
53+
val now = System.nanoTime()
54+
evictExpired(now)
55+
entries[key] = Entry(session, now + ttlNanos)
56+
while (entries.size > maxEntries) {
57+
entries.remove(entries.keys.first())
58+
}
59+
}
60+
61+
@Synchronized
62+
fun take(key: SabrPlaybackPrewarmKey): SabrPlaybackPrewarmSession? {
63+
val now = System.nanoTime()
64+
val entry = entries.remove(key) ?: return null
65+
return entry.session.takeUnless { now - entry.expiresAtNanos >= 0L }
66+
}
67+
68+
private fun evictExpired(now: Long) {
69+
val iterator = entries.values.iterator()
70+
while (iterator.hasNext()) {
71+
if (now - iterator.next().expiresAtNanos >= 0L) iterator.remove()
72+
}
73+
}
74+
75+
private companion object {
76+
const val MAX_ENTRIES = 64
77+
const val TTL_NANOS = 60_000_000_000L
78+
}
79+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.typetype.server.routes
2+
3+
import dev.typetype.server.services.SabrSessionHolder
4+
import io.ktor.server.application.ApplicationCall
5+
import io.ktor.server.request.receive
6+
7+
internal suspend fun ApplicationCall.playbackRequest(): SabrPlaybackRequest {
8+
val body = runCatching { receive<SabrPlaybackRequest>() }.getOrNull()
9+
return SabrPlaybackRequest(
10+
videoItag = body?.videoItag ?: request.queryParameters["videoItag"]?.toIntOrNull(),
11+
audioItag = body?.audioItag ?: request.queryParameters["audioItag"]?.toIntOrNull(),
12+
audioTrackId = body?.audioTrackId ?: request.queryParameters["audioTrackId"],
13+
startTimeMs = body?.startTimeMs ?: request.queryParameters["startTimeMs"]?.toLongOrNull(),
14+
playerTimeMs = body?.playerTimeMs ?: request.queryParameters["playerTimeMs"]?.toLongOrNull(),
15+
audioOnly = body?.audioOnly ?: request.queryParameters["audioOnly"]?.toBooleanStrictOrNull() ?: false,
16+
isLive = body?.isLive ?: request.queryParameters["isLive"]?.toBooleanStrictOrNull() ?: false,
17+
)
18+
}
19+
20+
internal fun SabrPlaybackRequest.effectiveStartTimeMs(): Long =
21+
(playerTimeMs ?: startTimeMs ?: 0L).coerceAtLeast(0L)
22+
23+
internal fun SabrPlaybackRequest.keepsCurrentFormats(holder: SabrSessionHolder): Boolean =
24+
(videoItag == null || videoItag == holder.videoFormat.itag) &&
25+
(audioItag == null || audioItag == holder.audioFormat.itag) &&
26+
(audioTrackId.isNullOrBlank() || audioTrackId == holder.audioFormat.audioTrackId)

0 commit comments

Comments
 (0)