mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Fixed VODs not working properly for YouTube and Twitch.
This commit is contained in:
parent
ecc94920d7
commit
9d5888ddf7
9 changed files with 95 additions and 75 deletions
|
@ -21,25 +21,4 @@ inline fun <reified T, R> 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)
|
||||
}
|
||||
}
|
|
@ -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!!);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<String, String>, 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())
|
||||
|
|
|
@ -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<String> {
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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<VariantPlaylistReference>()
|
||||
val mediaRenditions = mutableListOf<MediaRendition>()
|
||||
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 6732a56cd60522f995478399173dd020d8ffc828
|
||||
Subproject commit 8d978dd7bd749f837f13322742329c8f769a1a57
|
|
@ -1 +1 @@
|
|||
Subproject commit 6732a56cd60522f995478399173dd020d8ffc828
|
||||
Subproject commit 8d978dd7bd749f837f13322742329c8f769a1a57
|
Loading…
Add table
Reference in a new issue