Fixed VODs not working properly for YouTube and Twitch.

This commit is contained in:
Koen 2023-11-23 11:48:50 +01:00
commit 9d5888ddf7
9 changed files with 95 additions and 75 deletions

View file

@ -21,25 +21,4 @@ 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

@ -197,8 +197,13 @@ class HttpContext : AutoCloseable {
} }
fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) { fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) {
val bytes = body?.toByteArray(Charsets.UTF_8); val bytes = body?.toByteArray(Charsets.UTF_8);
if(body != null && headers.get("content-length").isNullOrEmpty()) if(headers.get("content-length").isNullOrEmpty()) {
headers.put("content-length", bytes!!.size.toString()); if (body != null) {
headers.put("content-length", bytes!!.size.toString());
} else {
headers.put("content-length", "0")
}
}
respond(status, headers) { responseStream -> respond(status, headers) { responseStream ->
if(body != null) { if(body != null) {
responseStream.write(bytes!!); responseStream.write(bytes!!);

View file

@ -4,17 +4,10 @@ import com.futo.platformplayer.api.http.server.HttpContext
class HttpOptionsAllowHandler(path: String) : HttpHandler("OPTIONS", path) { class HttpOptionsAllowHandler(path: String) : HttpHandler("OPTIONS", path) {
override fun handle(httpContext: HttpContext) { override fun handle(httpContext: HttpContext) {
//Just allow whatever is requested val newHeaders = headers.clone()
newHeaders.put("Access-Control-Allow-Origin", "*")
val requestedOrigin = httpContext.headers.getOrDefault("Access-Control-Request-Origin", ""); newHeaders.put("Access-Control-Allow-Methods", "*")
val requestedMethods = httpContext.headers.getOrDefault("Access-Control-Request-Method", ""); newHeaders.put("Access-Control-Allow-Headers", "*")
val requestedHeaders = httpContext.headers.getOrDefault("Access-Control-Request-Headers", "");
val newHeaders = headers.clone();
newHeaders.put("Allow", requestedMethods);
newHeaders.put("Access-Control-Allow-Methods", requestedMethods);
newHeaders.put("Access-Control-Allow-Headers", "*");
httpContext.respondCode(200, newHeaders); httpContext.respondCode(200, newHeaders);
} }
} }

View file

@ -98,11 +98,15 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
proxyHeaders.put("Referer", targetUrl); proxyHeaders.put("Referer", targetUrl);
val useMethod = if (method == "inherit") context.method else method; val useMethod = if (method == "inherit") context.method else method;
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${targetUrl}"); Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
Logger.i(TAG, "handleWithTcp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n")); Logger.i(TAG, "handleWithTcp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
makeTcpRequest(proxyHeaders, useMethod, parsed, context)
}
private fun makeTcpRequest(proxyHeaders: HashMap<String, String>, useMethod: String, parsed: Uri, context: HttpContext) {
val requestBuilder = StringBuilder() val requestBuilder = StringBuilder()
requestBuilder.append("$useMethod $targetUrl HTTP/1.1\r\n") requestBuilder.append("$useMethod $parsed HTTP/1.1\r\n")
proxyHeaders.forEach { (key, value) -> requestBuilder.append("$key: $value\r\n") } proxyHeaders.forEach { (key, value) -> requestBuilder.append("$key: $value\r\n") }
requestBuilder.append("\r\n") requestBuilder.append("\r\n")
@ -128,23 +132,31 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
val inputStream = s.getInputStream() val inputStream = s.getInputStream()
val resp = HttpResponseParser(inputStream) val resp = HttpResponseParser(inputStream)
val isChunked = resp.transferEncoding.equals("chunked", ignoreCase = true) if (resp.statusCode == 302) {
val contentLength = resp.contentLength.toInt() val location = resp.location!!
Logger.i(TAG, "handleWithTcp Proxied ${resp.statusCode} following redirect to $location");
makeTcpRequest(proxyHeaders, useMethod, Uri.parse(location)!!, context)
} else {
val isChunked = resp.transferEncoding.equals("chunked", ignoreCase = true)
val contentLength = resp.contentLength.toInt()
val headersFiltered = HttpHeaders(resp.headers.filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) }); val headersFiltered = HttpHeaders(resp.headers.filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) });
for(newHeader in headers) for (newHeader in headers)
headersFiltered.put(newHeader.key, newHeader.value); headersFiltered.put(newHeader.key, newHeader.value);
context.respond(resp.statusCode, headersFiltered) { responseStream -> context.respond(resp.statusCode, headersFiltered) { responseStream ->
if (isChunked) { if (isChunked) {
Logger.i(TAG, "handleWithTcp handleChunkedTransfer"); Logger.i(TAG, "handleWithTcp handleChunkedTransfer");
handleChunkedTransfer(inputStream, responseStream) handleChunkedTransfer(inputStream, responseStream)
} else if (contentLength != -1) { } else if (contentLength > 0) {
Logger.i(TAG, "handleWithTcp transferFixedLengthContent $contentLength"); Logger.i(TAG, "handleWithTcp transferFixedLengthContent $contentLength");
transferFixedLengthContent(inputStream, responseStream, contentLength) transferFixedLengthContent(inputStream, responseStream, contentLength)
} else { } else if (contentLength == -1) {
Logger.i(TAG, "handleWithTcp transferUntilEndOfStream"); Logger.i(TAG, "handleWithTcp transferUntilEndOfStream");
transferUntilEndOfStream(inputStream, responseStream) transferUntilEndOfStream(inputStream, responseStream)
} else {
Logger.i(TAG, "handleWithTcp no content");
}
} }
} }
} }
@ -156,7 +168,6 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
while (inputStream.readLine().also { line = it } != null) { while (inputStream.readLine().also { line = it } != null) {
val size = line!!.trim().toInt(16) val size = line!!.trim().toInt(16)
Logger.i(TAG, "handleWithTcp handleChunkedTransfer chunk size $size")
responseStream.write(line!!.encodeToByteArray()) responseStream.write(line!!.encodeToByteArray())
responseStream.write("\r\n".encodeToByteArray()) responseStream.write("\r\n".encodeToByteArray())

View file

@ -356,27 +356,35 @@ class StateCasting {
} }
} }
} else { } else {
if (videoSource is IVideoUrlSource) if (videoSource is IVideoUrlSource) {
Logger.i(TAG, "Casting as singular video");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble()); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble());
else if (audioSource is IAudioUrlSource) } else if (audioSource is IAudioUrlSource) {
Logger.i(TAG, "Casting as singular audio");
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 && video.isLive) { if (ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS");
castHlsIndirect(video, videoSource.url, resumePosition); castHlsIndirect(video, videoSource.url, resumePosition);
} else { } else {
Logger.i(TAG, "Casting as non-proxied HLS");
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 && video.isLive) { if (ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS");
castHlsIndirect(video, audioSource.url, resumePosition); castHlsIndirect(video, audioSource.url, resumePosition);
} else { } else {
Logger.i(TAG, "Casting as non-proxied audio HLS");
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());
} }
} else if (videoSource is LocalVideoSource) } else if (videoSource is LocalVideoSource) {
Logger.i(TAG, "Casting as local video");
castLocalVideo(video, videoSource, resumePosition); castLocalVideo(video, videoSource, resumePosition);
else if (audioSource is LocalAudioSource) } else if (audioSource is LocalAudioSource) {
Logger.i(TAG, "Casting as local audio");
castLocalAudio(video, audioSource, resumePosition); castLocalAudio(video, audioSource, resumePosition);
else { } else {
var str = listOf( var str = listOf(
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
@ -413,6 +421,14 @@ class StateCasting {
return true; return true;
} }
private fun castVideoIndirect() {
}
private fun castAudioIndirect() {
}
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double) : List<String> { private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
@ -634,7 +650,7 @@ class StateCasting {
}.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectMaster") }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectMaster")
Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath)."); Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath).");
ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble()); ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble());
return listOf(hlsUrl); return listOf(hlsUrl);
} }
@ -684,8 +700,6 @@ class StateCasting {
val proxyStreams = ad !is FastCastCastingDevice; val proxyStreams = ad !is FastCastCastingDevice;
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
Logger.i(TAG, "DASH url: $url");
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val dashPath = "/dash-${id}" val dashPath = "/dash-${id}"
@ -694,6 +708,8 @@ class StateCasting {
val subtitlePath = "/subtitle-${id}" val subtitlePath = "/subtitle-${id}"
val dashUrl = url + dashPath; val dashUrl = url + dashPath;
Logger.i(TAG, "DASH url: $dashUrl");
val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl(); val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl();
val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl(); val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl();
@ -719,6 +735,10 @@ class StateCasting {
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt") HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
.withHeader("Access-Control-Allow-Origin", "*"), true .withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast"); ).withTag("cast");
_castServer.addHandler(
HttpOptionsAllowHandler(subtitlePath)
.withHeader("Access-Control-Allow-Origin", "*")
).withTag("cast");
} }
subtitlesUrl = url + subtitlePath; subtitlesUrl = url + subtitlePath;
@ -732,28 +752,32 @@ class StateCasting {
"application/dash+xml") "application/dash+xml")
.withHeader("Access-Control-Allow-Origin", "*"), true .withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast"); ).withTag("cast");
_castServer.addHandler(
HttpOptionsAllowHandler(dashPath)
.withHeader("Access-Control-Allow-Origin", "*")
).withTag("cast");
if (videoSource != null) { if (videoSource != null) {
_castServer.addHandler( _castServer.addHandler(
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl()) HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
.withInjectedHost() .withInjectedHost()
.withHeader("Access-Control-Allow-Origin", "*"), true .withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast"); ).withTag("cast");
_castServer.addHandler( _castServer.addHandler(
HttpOptionsAllowHandler(videoPath) HttpOptionsAllowHandler(videoPath)
.withHeader("Access-Control-Allow-Origin", "*") .withHeader("Access-Control-Allow-Origin", "*")
.withHeader("Connection", "keep-alive")) ).withTag("cast");
.withTag("cast");
} }
if (audioSource != null) { if (audioSource != null) {
_castServer.addHandler( _castServer.addHandler(
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl()) HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
.withInjectedHost() .withInjectedHost()
.withHeader("Access-Control-Allow-Origin", "*"), true .withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast"); ).withTag("cast");
_castServer.addHandler( _castServer.addHandler(
HttpOptionsAllowHandler(audioPath) HttpOptionsAllowHandler(audioPath)
.withHeader("Access-Control-Allow-Origin", "*") .withHeader("Access-Control-Allow-Origin", "*")
.withHeader("Connection", "keep-alivcontexte")) )
.withTag("cast"); .withTag("cast");
} }

View file

@ -1,7 +1,6 @@
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
@ -15,7 +14,7 @@ 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 = sourceUrl.toURIRobust()!!.resolve("./").toString() val baseUrl = URI(sourceUrl).resolve("./").toString()
val variantPlaylists = mutableListOf<VariantPlaylistReference>() val variantPlaylists = mutableListOf<VariantPlaylistReference>()
val mediaRenditions = mutableListOf<MediaRendition>() val mediaRenditions = mutableListOf<MediaRendition>()
@ -81,7 +80,7 @@ class HLS {
} }
else -> { else -> {
currentSegment?.let { currentSegment?.let {
it.uri = line it.uri = resolveUrl(sourceUrl, line)
segments.add(it) segments.add(it)
} }
currentSegment = null currentSegment = null
@ -93,9 +92,16 @@ class HLS {
} }
private fun resolveUrl(baseUrl: String, url: String): String { private fun resolveUrl(baseUrl: String, url: String): String {
return if (url.toURIRobust()!!.isAbsolute) url else baseUrl + url val baseUri = URI(baseUrl)
} val urlUri = URI(url)
return if (urlUri.isAbsolute) {
url
} else {
val resolvedUri = baseUri.resolve(urlUri)
resolvedUri.toString()
}
}
private fun parseStreamInfo(content: String): StreamInfo { private fun parseStreamInfo(content: String): StreamInfo {
val attributes = parseAttributes(content) val attributes = parseAttributes(content)

View file

@ -14,6 +14,7 @@ class HttpResponseParser : AutoCloseable {
var contentType: String? = null; var contentType: String? = null;
var transferEncoding: String? = null; var transferEncoding: String? = null;
var location: String? = null;
var contentLength: Long = -1L; var contentLength: Long = -1L;
var statusCode: Int = -1; var statusCode: Int = -1;
@ -47,6 +48,7 @@ class HttpResponseParser : AutoCloseable {
"content-length" -> contentLength = headerValue.toLong(); "content-length" -> contentLength = headerValue.toLong();
"content-type" -> contentType = headerValue; "content-type" -> contentType = headerValue;
"transfer-encoding" -> transferEncoding = headerValue; "transfer-encoding" -> transferEncoding = headerValue;
"location" -> location = headerValue;
} }
if(line.isNullOrEmpty()) if(line.isNullOrEmpty())
break; break;

@ -1 +1 @@
Subproject commit 6732a56cd60522f995478399173dd020d8ffc828 Subproject commit 8d978dd7bd749f837f13322742329c8f769a1a57

@ -1 +1 @@
Subproject commit 6732a56cd60522f995478399173dd020d8ffc828 Subproject commit 8d978dd7bd749f837f13322742329c8f769a1a57