Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
e980084
feat: retrieve SABR streaming data from extractor
FineFindus Sep 8, 2025
0625f10
feat: add interface to native streaming library
FineFindus Sep 8, 2025
a6c4c3d
feat(player): setup basic SABR exoplayer infrastructure
FineFindus Sep 13, 2025
f41478e
feat(sabr): implement segments for media data
FineFindus Sep 16, 2025
5ca307e
chore(sabr): add infrastructure for chunked streaming
FineFindus Sep 16, 2025
580d87e
feat(sabr): create chunked sample streams
FineFindus Sep 16, 2025
129cc2a
feat(sabr): implement factory for DefaultSABRChunkSource
FineFindus Sep 16, 2025
492fc64
feat(sabr): use `trackSelection` to evaluate sabr chunk source methods
FineFindus Sep 22, 2025
49f7c8a
fixup
FineFindus Sep 22, 2025
0bcb189
chore: prefer SABR when available
FineFindus Sep 26, 2025
062f0b2
refactor(SABR): use Uri for creating sabr stream
FineFindus Sep 26, 2025
29c4ff1
wip: impl basic sources
FineFindus Sep 26, 2025
e9f3a7a
wip: use DASH
FineFindus Sep 26, 2025
68459ce
feat(sabr): add protobuf files
FineFindus Sep 29, 2025
412bf7f
feat(sabr): add UMP parser
FineFindus Sep 29, 2025
3ebf404
feat(sabr): add SABR client
FineFindus Sep 30, 2025
c796eec
fix(sabr): correctly set null xtags
FineFindus Sep 30, 2025
e3eeaad
fix(sabr): correctly retrieve new media segments
FineFindus Sep 30, 2025
8b7b23d
refactor(sabr): use verbose instead of debug logging
FineFindus Sep 30, 2025
f36b03d
feat(sabr): add Representation
FineFindus Oct 9, 2025
1181aae
feat(sabr): add AdaptationSet
FineFindus Oct 9, 2025
e7f7263
feat(sabr): add manifest
FineFindus Oct 9, 2025
a31057f
refactor(sabr): convert sabr client to singleton object
FineFindus Oct 12, 2025
422f03d
refactor(sabr): use representations to retrieve media data
FineFindus Oct 12, 2025
60bac75
fixup
FineFindus Oct 12, 2025
45229a6
feat(sabr): store and re-use sabr exception
FineFindus Oct 12, 2025
b8cbf21
feat(sabr): support retrieving DRC audio
FineFindus Oct 12, 2025
3ed7cc1
feat(sabr): only enable sabr streaming in debug mode
FineFindus Oct 17, 2025
45e2ee2
feat(sabr): implement data source
FineFindus Oct 17, 2025
691772a
refactor(sabr): port to Kotlin
FineFindus Oct 19, 2025
29011e7
refactor(sabr): remove JVM field tags
FineFindus Oct 19, 2025
0ee843a
fixup unused constructor param
FineFindus Oct 19, 2025
37169f0
refactor(sabr): remove unused constructors
FineFindus Oct 19, 2025
edeaa91
feat: generate content-bound poToken
FineFindus Oct 27, 2025
58c9dbe
feat(sabr): implement support for poTokens
FineFindus Oct 28, 2025
9083181
chore(proto): rename `lmt` to `last_modified` time
FineFindus Nov 12, 2025
79fd117
feat(sabr): only log unhandled UMP parts
FineFindus Nov 12, 2025
37aa692
feat(sabr): build and send BufferedRanges
FineFindus Nov 12, 2025
c94d651
fix: remove unused import
FineFindus Nov 12, 2025
609b623
refactor(sabr): extract parsing and handling of xtags
FineFindus Nov 13, 2025
57472f5
feat(sabr): handle formats supporting voice boost
FineFindus Nov 13, 2025
edc1495
refactor(sabr/xtags): remove unnecessary field
FineFindus Nov 13, 2025
f00f76a
refactor(sabr): mark Representation formatId as non-null
FineFindus Nov 13, 2025
48c4273
fix(sabr): disable SABR for live-streams
FineFindus Nov 21, 2025
e2b978f
refactor(sabr): mark `RepresentationHolder` as data class
FineFindus Nov 21, 2025
8d020b4
refactor(sabr): only handle a single segment per chunk
FineFindus Nov 21, 2025
b4dc4b6
refactor(sabr): use idiomatic continue on null selection
FineFindus Nov 21, 2025
861ef81
style: reformat code
FineFindus Nov 21, 2025
ab11f4e
feat(sabr): allow discontinuous playback
FineFindus Nov 21, 2025
21a533d
feat(sabr): synchronize buffered segments with chunksource queue
FineFindus Nov 21, 2025
af7f28d
feat(sabr): store init segment
FineFindus Nov 21, 2025
cb3217c
fix(sabr): use buffered and downloaded segments to build buffered ranges
FineFindus Nov 21, 2025
3746d03
feat(sabr): assert that we never load segments past the end of stream
FineFindus Nov 22, 2025
ddb7c81
fix(sabr): use segment start time to request
FineFindus Nov 22, 2025
6d7f909
feat(sabr): set `Origin` and `Referer` header
FineFindus Nov 22, 2025
27cfb67
build: update NewPipeExtractor
FineFindus Nov 22, 2025
1804a13
refactor(sabr): remove unnecessary null-safety assertion
FineFindus Nov 22, 2025
ef404d6
feat(sabr): set `elapsedWallTimeMs`
FineFindus Nov 22, 2025
fd51645
feat(sabr): avoid retrieving segments from buffered segments
FineFindus Nov 22, 2025
3fbe10f
style: reformat code
FineFindus Nov 22, 2025
708c38c
refactor(sabr): extract common format selection code
FineFindus Nov 22, 2025
44421aa
refactor(sabr): remove unused `poToken` from manifest
FineFindus Nov 23, 2025
cdfc9d0
feat(sabr): inform server about last seek timestamp
FineFindus Nov 23, 2025
b2f7cd6
refactor(sabr): cleanup
FineFindus Nov 23, 2025
b6fcdf6
fix(sabr): correctly set xtags from extractor
FineFindus Nov 23, 2025
6e8a207
refactor(sabr): create `FormatId` for `Representation` directly from …
FineFindus Nov 23, 2025
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
25 changes: 25 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.google.protobuf.gradle.id

plugins {
id("com.android.application")
id("kotlin-android")
Expand All @@ -6,6 +8,7 @@ plugins {
id("androidx.navigation.safeargs.kotlin")
alias(libs.plugins.baselineprofile)
alias(libs.plugins.ksp)
alias(libs.plugins.google.protobuf)
}

android {
Expand Down Expand Up @@ -136,6 +139,8 @@ dependencies {
implementation(libs.kotlinx.serialization)
implementation(libs.kotlinx.datetime)
implementation(libs.converter.kotlinx.serialization)
implementation(libs.google.protobuf.javalite)
implementation(libs.google.protobuf.kotlin.lite)

/* NewPipe Extractor */
implementation(libs.newpipeextractor)
Expand All @@ -160,3 +165,23 @@ dependencies {
/* Testing */
testImplementation(libs.junit)
}

//TODO: exclude from release protobuf
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().forEach { task ->
task.plugins {
//TODO: only generate kotlin code
id("java") {
option("lite")
}
// id("kotlin") {
// option("lite")
// }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ private fun VideoStream.toPipedStream() = PipedStream(
indexStart = indexStart,
indexEnd = indexEnd,
fps = fps,
contentLength = itagItem?.contentLength ?: 0L
contentLength = itagItem?.contentLength ?: 0L,
itag = itagItem?.id,
lastModified = itagItem?.lastModified,
xtags = itagItem?.xtags,
)

private fun AudioStream.toPipedStream() = PipedStream(
Expand All @@ -89,7 +92,11 @@ private fun AudioStream.toPipedStream() = PipedStream(
audioTrackName = audioTrackName,
audioTrackLocale = audioLocale?.toLanguageTag(),
audioTrackType = audioTrackType?.name,
videoOnly = false
videoOnly = false,
itag = itagItem?.id,
lastModified = itagItem?.lastModified,
isDrc = itagItem?.isDrc,
xtags = itagItem?.xtags,
)

fun StreamInfoItem.toStreamItem(
Expand Down Expand Up @@ -328,8 +335,8 @@ class NewPipeMediaServiceRepository : MediaServiceRepository {
)
},
audioStreams = resp.audioStreams.map { it.toPipedStream() },
videoStreams = resp.videoOnlyStreams.map { it.toPipedStream().copy(videoOnly = true) } +
resp.videoStreams.map { it.toPipedStream().copy(videoOnly = false) },
videoStreams = resp.videoOnlyStreams.map { it.toPipedStream().copy(videoOnly = true,) },
//+ resp.videoStreams.map { it.toPipedStream().copy(videoOnly = false) },
previewFrames = resp.previewFrames.map {
PreviewFrames(
it.urls,
Expand All @@ -349,7 +356,9 @@ class NewPipeMediaServiceRepository : MediaServiceRepository {
it.languageTag,
it.isAutoGenerated
)
}
},
serverAbrStreamingUrl = resp.serverAbrStreamingUrl,
videoPlaybackUstreamerConfig = resp.ustreamerConfig,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ data class PipedStream(
val audioTrackId: String? = null,
val contentLength: Long = -1,
val audioTrackType: String? = null,
val audioTrackLocale: String? = null
val audioTrackLocale: String? = null,
val itag: Int? = null,
val lastModified: Long? = null,
val isDrc: Boolean? = null,
val xtags: String? = null,
): Parcelable {
private fun getQualityString(videoId: String): String {
return "${videoId}_${quality?.replace(" ", "_")}_$format." +
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/github/libretube/api/obj/Streams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ data class Streams(
val proxyUrl: String? = null,
val chapters: List<ChapterSegment> = emptyList(),
val uploaderSubscriberCount: Long = 0,
val previewFrames: List<PreviewFrames> = emptyList()
val previewFrames: List<PreviewFrames> = emptyList(),
val serverAbrStreamingUrl: String? = null,
val videoPlaybackUstreamerConfig: String? = null
): Parcelable {
@IgnoredOnParcel
val isLive = livestream || duration <= 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class PoTokenGenerator : PoTokenProvider {

private object WebPoTokenGenLock
private var webPoTokenVisitorData: String? = null
private var webPoTokenStreamingPot: String? = null
private var webPoTokenGenerator: PoTokenWebView? = null


Expand All @@ -37,10 +36,7 @@ class PoTokenGenerator : PoTokenProvider {
* [PoTokenGenerator.getWebClientPoToken] was called
*/
private fun getWebClientPoToken(videoId: String, forceRecreate: Boolean): PoTokenResult {
// just a helper class since Kotlin does not have builtin support for 4-tuples
data class Quadruple<T1, T2, T3, T4>(val t1: T1, val t2: T2, val t3: T3, val t4: T4)

val (poTokenGenerator, visitorData, streamingPot, hasBeenRecreated) =
val (poTokenGenerator, visitorData, hasBeenRecreated) =
synchronized(WebPoTokenGenLock) {
val shouldRecreate = webPoTokenGenerator == null || forceRecreate || webPoTokenGenerator!!.isExpired()

Expand All @@ -66,22 +62,17 @@ class PoTokenGenerator : PoTokenProvider {
// create a new webPoTokenGenerator
webPoTokenGenerator = PoTokenWebView
.newPoTokenGenerator(LibreTubeApp.instance)

// The streaming poToken needs to be generated exactly once before generating
// any other (player) tokens.
webPoTokenStreamingPot = webPoTokenGenerator!!.generatePoToken(webPoTokenVisitorData!!)
}
}

return@synchronized Quadruple(
return@synchronized Triple(
webPoTokenGenerator!!,
webPoTokenVisitorData!!,
webPoTokenStreamingPot!!,
shouldRecreate
)
}

val playerPot = try {
val poToken = try {
// Not using synchronized here, since poTokenGenerator would be able to generate
// multiple poTokens in parallel if needed. The only important thing is for exactly one
// visitorData/streaming poToken to be generated before anything else.
Expand All @@ -105,13 +96,11 @@ class PoTokenGenerator : PoTokenProvider {

if (BuildConfig.DEBUG) {
Log.d(
TAG,
"poToken for $videoId: playerPot=$playerPot, " +
"streamingPot=$streamingPot, visitor_data=$visitorData"
TAG, "poToken for $videoId: $poToken, visitor_data=$visitorData"
)
}

return PoTokenResult(visitorData, playerPot, streamingPot)
return PoTokenResult(visitorData, poToken, poToken)
}

override fun getWebEmbedClientPoToken(videoId: String?): PoTokenResult? = null
Expand Down
Loading
Loading