mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-03 22:59:56 +00:00
Further extended HLS spec that is implemented.
This commit is contained in:
parent
e221b508d3
commit
fad1b216df
3 changed files with 87 additions and 22 deletions
|
@ -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)
|
||||||
|
@ -17,3 +22,24 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,6 +606,7 @@ class StateCasting {
|
||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
val newPlaylistUrl = url + newPlaylistPath;
|
val newPlaylistUrl = url + newPlaylistPath;
|
||||||
|
|
||||||
|
if (mediaRendition.uri != null) {
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
@ -615,6 +616,7 @@ class StateCasting {
|
||||||
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,13 +639,17 @@ 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>()
|
||||||
|
|
||||||
|
if (proxySegments) {
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||||
val sequenceNumber = variantPlaylist.mediaSequence + index.toLong()
|
val sequenceNumber = variantPlaylist.mediaSequence + index.toLong()
|
||||||
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
newSegments.addAll(variantPlaylist.segments)
|
||||||
|
}
|
||||||
|
|
||||||
return HLS.VariantPlaylist(
|
return HLS.VariantPlaylist(
|
||||||
variantPlaylist.version,
|
variantPlaylist.version,
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue