diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt index 0b79de90..6ce03b70 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt @@ -1,5 +1,10 @@ package com.futo.platformplayer +import android.net.Uri +import java.net.URI +import java.net.URISyntaxException +import java.net.URLEncoder + //Syntax sugaring inline fun Any.assume(): T?{ if(this is T) @@ -16,4 +21,25 @@ inline fun Any.assume(cb: (T) -> R): R? { fun String?.yesNoToBoolean(): Boolean { 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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 34b3da87..ad136bdb 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -361,13 +361,13 @@ class StateCasting { else if (audioSource is IAudioUrlSource) ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble()); else if(videoSource is IHLSManifestSource) { - if (ad is ChromecastCastingDevice) { + if (ad is ChromecastCastingDevice && video.isLive) { castHlsIndirect(video, videoSource.url, resumePosition); } else { ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble()); } } else if(audioSource is IHLSManifestAudioSource) { - if (ad is ChromecastCastingDevice) { + if (ad is ChromecastCastingDevice && video.isLive) { castHlsIndirect(video, audioSource.url, resumePosition); } else { 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 newVariantPlaylistRefs = arrayListOf() val newMediaRenditions = arrayListOf() - val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.independentSegments) + val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.sessionDataList, masterPlaylist.independentSegments) for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) { val playlistId = UUID.randomUUID(); @@ -606,15 +606,17 @@ class StateCasting { val newPlaylistPath = "/hls-playlist-${playlistId}" val newPlaylistUrl = url + newPlaylistPath; - _castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext -> - val vpHeaders = vpContext.headers.clone() - vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; + if (mediaRendition.uri != null) { + _castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext -> + val vpHeaders = vpContext.headers.clone() + vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri) - val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist) - val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant") + val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri) + val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist) + val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() + vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); + }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant") + } newMediaRenditions.add(HLS.MediaRendition( mediaRendition.type, @@ -637,12 +639,16 @@ class StateCasting { 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() - variantPlaylist.segments.forEachIndexed { index, segment -> - val sequenceNumber = variantPlaylist.mediaSequence + index.toLong() - newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) + if (proxySegments) { + variantPlaylist.segments.forEachIndexed { index, segment -> + val sequenceNumber = variantPlaylist.mediaSequence + index.toLong() + newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) + } + } else { + newSegments.addAll(variantPlaylist.segments) } return HLS.VariantPlaylist( diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index c3fa6245..6f37e815 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -1,6 +1,7 @@ package com.futo.platformplayer.parsers import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.toURIRobust import com.futo.platformplayer.yesNoToBoolean import java.net.URI import java.time.ZonedDateTime @@ -14,10 +15,11 @@ class HLS { val masterPlaylistContent = masterPlaylistResponse.body?.string() ?: throw Exception("Master playlist content is empty") - val baseUrl = URI(sourceUrl).resolve("./").toString() + val baseUrl = sourceUrl.toURIRobust()!!.resolve("./").toString() val variantPlaylists = mutableListOf() val mediaRenditions = mutableListOf() + val sessionDataList = mutableListOf() var independentSegments = false masterPlaylistContent.lines().forEachIndexed { index, line -> @@ -37,10 +39,15 @@ class HLS { line == "#EXT-X-INDEPENDENT-SEGMENTS" -> { 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 { @@ -86,7 +93,7 @@ class HLS { } 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 { val attributes = parseAttributes(line) - val uri = attributes["URI"]!! - val url = resolveUrl(baseUrl, uri) + val uri = attributes["URI"]?.let { resolveUrl(baseUrl, it) } return MediaRendition( type = attributes["TYPE"], - uri = url, + uri = uri, groupID = attributes["GROUP-ID"], language = attributes["LANGUAGE"], 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 { val attributes = mutableMapOf() 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( val bandwidth: Int?, val resolution: String?, @@ -170,7 +197,7 @@ class HLS { data class MediaRendition( val type: String?, - val uri: String, + val uri: String?, val groupID: String?, val language: String?, val name: String?, @@ -194,9 +221,11 @@ class HLS { } } + data class MasterPlaylist( val variantPlaylistsRefs: List, val mediaRenditions: List, + val sessionDataList: List, val independentSegments: Boolean ) { fun buildM3U8(): String { @@ -214,6 +243,10 @@ class HLS { builder.append(variant.toM3U8Line()) } + sessionDataList.forEach { data -> + builder.append(data.toM3U8Line()) + } + return builder.toString() } }