Further extended HLS spec that is implemented.

This commit is contained in:
Koen 2023-11-22 09:32:52 +01:00
commit fad1b216df
3 changed files with 87 additions and 22 deletions

View file

@ -1,5 +1,10 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.net.Uri
import java.net.URI
import java.net.URISyntaxException
import java.net.URLEncoder
//Syntax sugaring //Syntax sugaring
inline fun <reified T> Any.assume(): T?{ inline fun <reified T> Any.assume(): T?{
if(this is T) if(this is T)
@ -16,4 +21,25 @@ inline fun <reified T, R> Any.assume(cb: (T) -> R): R? {
fun String?.yesNoToBoolean(): Boolean { fun String?.yesNoToBoolean(): Boolean {
return this?.uppercase() == "YES" return this?.uppercase() == "YES"
}
fun String?.toURIRobust(): URI? {
if (this == null) {
return null
}
try {
return URI(this)
} catch (e: URISyntaxException) {
val parts = this.split("\\?".toRegex(), 2)
if (parts.size < 2) {
return null
}
val beforeQuery = parts[0]
val query = parts[1]
val encodedQuery = URLEncoder.encode(query, "UTF-8")
val rebuiltUrl = "$beforeQuery?$encodedQuery"
return URI(rebuiltUrl)
}
} }

View file

@ -361,13 +361,13 @@ class StateCasting {
else if (audioSource is IAudioUrlSource) else if (audioSource is IAudioUrlSource)
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble()); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
else if(videoSource is IHLSManifestSource) { else if(videoSource is IHLSManifestSource) {
if (ad is ChromecastCastingDevice) { if (ad is ChromecastCastingDevice && video.isLive) {
castHlsIndirect(video, videoSource.url, resumePosition); castHlsIndirect(video, videoSource.url, resumePosition);
} else { } else {
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble()); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble());
} }
} else if(audioSource is IHLSManifestAudioSource) { } else if(audioSource is IHLSManifestAudioSource) {
if (ad is ChromecastCastingDevice) { if (ad is ChromecastCastingDevice && video.isLive) {
castHlsIndirect(video, audioSource.url, resumePosition); castHlsIndirect(video, audioSource.url, resumePosition);
} else { } else {
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble()); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble());
@ -578,7 +578,7 @@ class StateCasting {
val masterPlaylist = HLS.downloadAndParseMasterPlaylist(_client, sourceUrl) val masterPlaylist = HLS.downloadAndParseMasterPlaylist(_client, sourceUrl)
val newVariantPlaylistRefs = arrayListOf<HLS.VariantPlaylistReference>() val newVariantPlaylistRefs = arrayListOf<HLS.VariantPlaylistReference>()
val newMediaRenditions = arrayListOf<HLS.MediaRendition>() val newMediaRenditions = arrayListOf<HLS.MediaRendition>()
val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.independentSegments) val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.sessionDataList, masterPlaylist.independentSegments)
for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) { for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) {
val playlistId = UUID.randomUUID(); val playlistId = UUID.randomUUID();
@ -606,15 +606,17 @@ class StateCasting {
val newPlaylistPath = "/hls-playlist-${playlistId}" val newPlaylistPath = "/hls-playlist-${playlistId}"
val newPlaylistUrl = url + newPlaylistPath; val newPlaylistUrl = url + newPlaylistPath;
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext -> if (mediaRendition.uri != null) {
val vpHeaders = vpContext.headers.clone() _castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; val vpHeaders = vpContext.headers.clone()
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri) val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri)
val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist) val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist)
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
}.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant") }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant")
}
newMediaRenditions.add(HLS.MediaRendition( newMediaRenditions.add(HLS.MediaRendition(
mediaRendition.type, mediaRendition.type,
@ -637,12 +639,16 @@ class StateCasting {
return listOf(hlsUrl); return listOf(hlsUrl);
} }
private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist): HLS.VariantPlaylist { private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, proxySegments: Boolean = true): HLS.VariantPlaylist {
val newSegments = arrayListOf<HLS.Segment>() val newSegments = arrayListOf<HLS.Segment>()
variantPlaylist.segments.forEachIndexed { index, segment -> if (proxySegments) {
val sequenceNumber = variantPlaylist.mediaSequence + index.toLong() variantPlaylist.segments.forEachIndexed { index, segment ->
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) val sequenceNumber = variantPlaylist.mediaSequence + index.toLong()
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
}
} else {
newSegments.addAll(variantPlaylist.segments)
} }
return HLS.VariantPlaylist( return HLS.VariantPlaylist(

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer.parsers package com.futo.platformplayer.parsers
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.toURIRobust
import com.futo.platformplayer.yesNoToBoolean import com.futo.platformplayer.yesNoToBoolean
import java.net.URI import java.net.URI
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -14,10 +15,11 @@ class HLS {
val masterPlaylistContent = masterPlaylistResponse.body?.string() val masterPlaylistContent = masterPlaylistResponse.body?.string()
?: throw Exception("Master playlist content is empty") ?: throw Exception("Master playlist content is empty")
val baseUrl = URI(sourceUrl).resolve("./").toString() val baseUrl = sourceUrl.toURIRobust()!!.resolve("./").toString()
val variantPlaylists = mutableListOf<VariantPlaylistReference>() val variantPlaylists = mutableListOf<VariantPlaylistReference>()
val mediaRenditions = mutableListOf<MediaRendition>() val mediaRenditions = mutableListOf<MediaRendition>()
val sessionDataList = mutableListOf<SessionData>()
var independentSegments = false var independentSegments = false
masterPlaylistContent.lines().forEachIndexed { index, line -> masterPlaylistContent.lines().forEachIndexed { index, line ->
@ -37,10 +39,15 @@ class HLS {
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> { line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
independentSegments = true independentSegments = true
} }
line.startsWith("#EXT-X-SESSION-DATA") -> {
val sessionData = parseSessionData(line)
sessionDataList.add(sessionData)
}
} }
} }
return MasterPlaylist(variantPlaylists, mediaRenditions, independentSegments) return MasterPlaylist(variantPlaylists, mediaRenditions, sessionDataList, independentSegments)
} }
fun downloadAndParseVariantPlaylist(client: ManagedHttpClient, sourceUrl: String): VariantPlaylist { fun downloadAndParseVariantPlaylist(client: ManagedHttpClient, sourceUrl: String): VariantPlaylist {
@ -86,7 +93,7 @@ class HLS {
} }
private fun resolveUrl(baseUrl: String, url: String): String { private fun resolveUrl(baseUrl: String, url: String): String {
return if (URI(url).isAbsolute) url else baseUrl + url return if (url.toURIRobust()!!.isAbsolute) url else baseUrl + url
} }
@ -105,11 +112,10 @@ class HLS {
private fun parseMediaRendition(client: ManagedHttpClient, line: String, baseUrl: String): MediaRendition { private fun parseMediaRendition(client: ManagedHttpClient, line: String, baseUrl: String): MediaRendition {
val attributes = parseAttributes(line) val attributes = parseAttributes(line)
val uri = attributes["URI"]!! val uri = attributes["URI"]?.let { resolveUrl(baseUrl, it) }
val url = resolveUrl(baseUrl, uri)
return MediaRendition( return MediaRendition(
type = attributes["TYPE"], type = attributes["TYPE"],
uri = url, uri = uri,
groupID = attributes["GROUP-ID"], groupID = attributes["GROUP-ID"],
language = attributes["LANGUAGE"], language = attributes["LANGUAGE"],
name = attributes["NAME"], name = attributes["NAME"],
@ -119,6 +125,13 @@ class HLS {
) )
} }
private fun parseSessionData(line: String): SessionData {
val attributes = parseAttributes(line)
val dataId = attributes["DATA-ID"]!!
val value = attributes["VALUE"]!!
return SessionData(dataId, value)
}
private fun parseAttributes(content: String): Map<String, String> { private fun parseAttributes(content: String): Map<String, String> {
val attributes = mutableMapOf<String, String>() val attributes = mutableMapOf<String, String>()
val attributePairs = content.substringAfter(":").splitToSequence(',') val attributePairs = content.substringAfter(":").splitToSequence(',')
@ -158,6 +171,20 @@ class HLS {
} }
} }
data class SessionData(
val dataId: String,
val value: String
) {
fun toM3U8Line(): String = buildString {
append("#EXT-X-SESSION-DATA:")
appendAttributes(this,
"DATA-ID" to dataId,
"VALUE" to value
)
append("\n")
}
}
data class StreamInfo( data class StreamInfo(
val bandwidth: Int?, val bandwidth: Int?,
val resolution: String?, val resolution: String?,
@ -170,7 +197,7 @@ class HLS {
data class MediaRendition( data class MediaRendition(
val type: String?, val type: String?,
val uri: String, val uri: String?,
val groupID: String?, val groupID: String?,
val language: String?, val language: String?,
val name: String?, val name: String?,
@ -194,9 +221,11 @@ class HLS {
} }
} }
data class MasterPlaylist( data class MasterPlaylist(
val variantPlaylistsRefs: List<VariantPlaylistReference>, val variantPlaylistsRefs: List<VariantPlaylistReference>,
val mediaRenditions: List<MediaRendition>, val mediaRenditions: List<MediaRendition>,
val sessionDataList: List<SessionData>,
val independentSegments: Boolean val independentSegments: Boolean
) { ) {
fun buildM3U8(): String { fun buildM3U8(): String {
@ -214,6 +243,10 @@ class HLS {
builder.append(variant.toM3U8Line()) builder.append(variant.toM3U8Line())
} }
sessionDataList.forEach { data ->
builder.append(data.toM3U8Line())
}
return builder.toString() return builder.toString()
} }
} }