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 6ce03b70..0fc8dcd8 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt @@ -21,25 +21,4 @@ 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/api/http/server/HttpContext.kt b/app/src/main/java/com/futo/platformplayer/api/http/server/HttpContext.kt index f08610f5..ecb835c4 100644 --- a/app/src/main/java/com/futo/platformplayer/api/http/server/HttpContext.kt +++ b/app/src/main/java/com/futo/platformplayer/api/http/server/HttpContext.kt @@ -197,8 +197,13 @@ class HttpContext : AutoCloseable { } fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) { val bytes = body?.toByteArray(Charsets.UTF_8); - if(body != null && headers.get("content-length").isNullOrEmpty()) - headers.put("content-length", bytes!!.size.toString()); + if(headers.get("content-length").isNullOrEmpty()) { + if (body != null) { + headers.put("content-length", bytes!!.size.toString()); + } else { + headers.put("content-length", "0") + } + } respond(status, headers) { responseStream -> if(body != null) { responseStream.write(bytes!!); diff --git a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpOptionsAllowHandler.kt b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpOptionsAllowHandler.kt index af226aa6..3561e31e 100644 --- a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpOptionsAllowHandler.kt +++ b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpOptionsAllowHandler.kt @@ -4,17 +4,10 @@ import com.futo.platformplayer.api.http.server.HttpContext class HttpOptionsAllowHandler(path: String) : HttpHandler("OPTIONS", path) { override fun handle(httpContext: HttpContext) { - //Just allow whatever is requested - - val requestedOrigin = httpContext.headers.getOrDefault("Access-Control-Request-Origin", ""); - val requestedMethods = httpContext.headers.getOrDefault("Access-Control-Request-Method", ""); - 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", "*"); - + val newHeaders = headers.clone() + newHeaders.put("Access-Control-Allow-Origin", "*") + newHeaders.put("Access-Control-Allow-Methods", "*") + newHeaders.put("Access-Control-Allow-Headers", "*") httpContext.respondCode(200, newHeaders); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt index afc2589e..74dcbb52 100644 --- a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt +++ b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt @@ -98,11 +98,15 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv proxyHeaders.put("Referer", targetUrl); 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")); + makeTcpRequest(proxyHeaders, useMethod, parsed, context) + } + + private fun makeTcpRequest(proxyHeaders: HashMap, useMethod: String, parsed: Uri, context: HttpContext) { 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") } requestBuilder.append("\r\n") @@ -128,23 +132,31 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv val inputStream = s.getInputStream() val resp = HttpResponseParser(inputStream) - val isChunked = resp.transferEncoding.equals("chunked", ignoreCase = true) - val contentLength = resp.contentLength.toInt() + if (resp.statusCode == 302) { + 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()) }); - for(newHeader in headers) - headersFiltered.put(newHeader.key, newHeader.value); + val headersFiltered = HttpHeaders(resp.headers.filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) }); + for (newHeader in headers) + headersFiltered.put(newHeader.key, newHeader.value); - context.respond(resp.statusCode, headersFiltered) { responseStream -> - if (isChunked) { - Logger.i(TAG, "handleWithTcp handleChunkedTransfer"); - handleChunkedTransfer(inputStream, responseStream) - } else if (contentLength != -1) { - Logger.i(TAG, "handleWithTcp transferFixedLengthContent $contentLength"); - transferFixedLengthContent(inputStream, responseStream, contentLength) - } else { - Logger.i(TAG, "handleWithTcp transferUntilEndOfStream"); - transferUntilEndOfStream(inputStream, responseStream) + context.respond(resp.statusCode, headersFiltered) { responseStream -> + if (isChunked) { + Logger.i(TAG, "handleWithTcp handleChunkedTransfer"); + handleChunkedTransfer(inputStream, responseStream) + } else if (contentLength > 0) { + Logger.i(TAG, "handleWithTcp transferFixedLengthContent $contentLength"); + transferFixedLengthContent(inputStream, responseStream, contentLength) + } else if (contentLength == -1) { + Logger.i(TAG, "handleWithTcp transferUntilEndOfStream"); + 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) { val size = line!!.trim().toInt(16) - Logger.i(TAG, "handleWithTcp handleChunkedTransfer chunk size $size") responseStream.write(line!!.encodeToByteArray()) responseStream.write("\r\n".encodeToByteArray()) 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 ad136bdb..9039e3ca 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -356,27 +356,35 @@ class StateCasting { } } } 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()); - 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()); - else if(videoSource is IHLSManifestSource) { - if (ad is ChromecastCastingDevice && video.isLive) { + } else if(videoSource is IHLSManifestSource) { + if (ad is ChromecastCastingDevice) { + Logger.i(TAG, "Casting as proxied HLS"); castHlsIndirect(video, videoSource.url, resumePosition); } 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()); } } 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); } 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()); } - } else if (videoSource is LocalVideoSource) + } else if (videoSource is LocalVideoSource) { + Logger.i(TAG, "Casting as local video"); 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); - else { + } else { var str = listOf( if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, @@ -413,6 +421,14 @@ class StateCasting { return true; } + private fun castVideoIndirect() { + + } + + private fun castAudioIndirect() { + + } + private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double) : List { val ad = activeDevice ?: return listOf(); @@ -634,7 +650,7 @@ class StateCasting { }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectMaster") 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); } @@ -684,8 +700,6 @@ class StateCasting { val proxyStreams = ad !is FastCastCastingDevice; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; - Logger.i(TAG, "DASH url: $url"); - val id = UUID.randomUUID(); val dashPath = "/dash-${id}" @@ -694,6 +708,8 @@ class StateCasting { val subtitlePath = "/subtitle-${id}" val dashUrl = url + dashPath; + Logger.i(TAG, "DASH url: $dashUrl"); + val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl(); val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl(); @@ -719,6 +735,10 @@ class StateCasting { HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt") .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); + _castServer.addHandler( + HttpOptionsAllowHandler(subtitlePath) + .withHeader("Access-Control-Allow-Origin", "*") + ).withTag("cast"); } subtitlesUrl = url + subtitlePath; @@ -732,28 +752,32 @@ class StateCasting { "application/dash+xml") .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); + _castServer.addHandler( + HttpOptionsAllowHandler(dashPath) + .withHeader("Access-Control-Allow-Origin", "*") + ).withTag("cast"); + if (videoSource != null) { _castServer.addHandler( - HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl()) + HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); _castServer.addHandler( HttpOptionsAllowHandler(videoPath) .withHeader("Access-Control-Allow-Origin", "*") - .withHeader("Connection", "keep-alive")) - .withTag("cast"); + ).withTag("cast"); } if (audioSource != null) { _castServer.addHandler( - HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl()) + HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); _castServer.addHandler( HttpOptionsAllowHandler(audioPath) .withHeader("Access-Control-Allow-Origin", "*") - .withHeader("Connection", "keep-alivcontexte")) + ) .withTag("cast"); } 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 6f37e815..1deecc20 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -1,7 +1,6 @@ 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 @@ -15,7 +14,7 @@ class HLS { val masterPlaylistContent = masterPlaylistResponse.body?.string() ?: throw Exception("Master playlist content is empty") - val baseUrl = sourceUrl.toURIRobust()!!.resolve("./").toString() + val baseUrl = URI(sourceUrl).resolve("./").toString() val variantPlaylists = mutableListOf() val mediaRenditions = mutableListOf() @@ -81,7 +80,7 @@ class HLS { } else -> { currentSegment?.let { - it.uri = line + it.uri = resolveUrl(sourceUrl, line) segments.add(it) } currentSegment = null @@ -93,9 +92,16 @@ class HLS { } 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 { val attributes = parseAttributes(content) diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HttpResponseParser.kt b/app/src/main/java/com/futo/platformplayer/parsers/HttpResponseParser.kt index 2209ba24..bc3be71c 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HttpResponseParser.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HttpResponseParser.kt @@ -14,6 +14,7 @@ class HttpResponseParser : AutoCloseable { var contentType: String? = null; var transferEncoding: String? = null; + var location: String? = null; var contentLength: Long = -1L; var statusCode: Int = -1; @@ -47,6 +48,7 @@ class HttpResponseParser : AutoCloseable { "content-length" -> contentLength = headerValue.toLong(); "content-type" -> contentType = headerValue; "transfer-encoding" -> transferEncoding = headerValue; + "location" -> location = headerValue; } if(line.isNullOrEmpty()) break; diff --git a/app/src/stable/assets/sources/twitch b/app/src/stable/assets/sources/twitch index 6732a56c..8d978dd7 160000 --- a/app/src/stable/assets/sources/twitch +++ b/app/src/stable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 6732a56cd60522f995478399173dd020d8ffc828 +Subproject commit 8d978dd7bd749f837f13322742329c8f769a1a57 diff --git a/app/src/unstable/assets/sources/twitch b/app/src/unstable/assets/sources/twitch index 6732a56c..8d978dd7 160000 --- a/app/src/unstable/assets/sources/twitch +++ b/app/src/unstable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 6732a56cd60522f995478399173dd020d8ffc828 +Subproject commit 8d978dd7bd749f837f13322742329c8f769a1a57