mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Added OPTIONS headers where necessary and further HLS spec implementations.
This commit is contained in:
parent
eb3dd854d4
commit
e47349d010
4 changed files with 151 additions and 132 deletions
|
@ -5,6 +5,7 @@ import com.futo.platformplayer.api.http.ManagedHttpClient
|
|||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
|
@ -141,6 +142,23 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
fun addHandlerWithAllowAllOptions(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
|
||||
val allowedMethods = arrayListOf(handler.method, "OPTIONS")
|
||||
if (withHEAD) {
|
||||
allowedMethods.add("HEAD")
|
||||
}
|
||||
|
||||
val tag = handler.tag
|
||||
if (tag != null) {
|
||||
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods).withTag(tag))
|
||||
} else {
|
||||
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods))
|
||||
}
|
||||
|
||||
return addHandler(handler, withHEAD)
|
||||
}
|
||||
|
||||
fun removeHandler(method: String, path: String) {
|
||||
synchronized(_handlers) {
|
||||
val handlerMap = _handlers[method] ?: return
|
||||
|
|
|
@ -2,11 +2,17 @@ package com.futo.platformplayer.api.http.server.handlers
|
|||
|
||||
import com.futo.platformplayer.api.http.server.HttpContext
|
||||
|
||||
class HttpOptionsAllowHandler(path: String) : HttpHandler("OPTIONS", path) {
|
||||
class HttpOptionsAllowHandler(path: String, val allowedMethods: List<String> = listOf()) : HttpHandler("OPTIONS", path) {
|
||||
override fun handle(httpContext: HttpContext) {
|
||||
val newHeaders = headers.clone()
|
||||
newHeaders.put("Access-Control-Allow-Origin", "*")
|
||||
newHeaders.put("Access-Control-Allow-Methods", "*")
|
||||
|
||||
if (allowedMethods.isNotEmpty()) {
|
||||
newHeaders.put("Access-Control-Allow-Methods", allowedMethods.map { it.uppercase() }.joinToString(", "))
|
||||
} else {
|
||||
newHeaders.put("Access-Control-Allow-Methods", "*")
|
||||
}
|
||||
|
||||
newHeaders.put("Access-Control-Allow-Headers", "*")
|
||||
httpContext.respondCode(200, newHeaders);
|
||||
}
|
||||
|
|
|
@ -370,7 +370,7 @@ class StateCasting {
|
|||
} else if(videoSource is IHLSManifestSource) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied HLS");
|
||||
castProxiedHls(video, videoSource.url, resumePosition);
|
||||
castProxiedHls(video, videoSource.url, videoSource.codec, 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());
|
||||
|
@ -378,7 +378,7 @@ class StateCasting {
|
|||
} else if(audioSource is IHLSManifestAudioSource) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||
castProxiedHls(video, audioSource.url, resumePosition);
|
||||
castProxiedHls(video, audioSource.url, audioSource.codec, 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());
|
||||
|
@ -442,7 +442,7 @@ class StateCasting {
|
|||
val videoPath = "/video-${id}"
|
||||
val videoUrl = url + videoPath;
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
|
@ -461,7 +461,7 @@ class StateCasting {
|
|||
val audioPath = "/audio-${id}"
|
||||
val audioUrl = url + audioPath;
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
|
@ -492,7 +492,7 @@ class StateCasting {
|
|||
val variantPlaylistReferences = arrayListOf<HLS.VariantPlaylistReference>()
|
||||
|
||||
if (videoSource != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castLocalHls")
|
||||
|
@ -501,9 +501,9 @@ class StateCasting {
|
|||
val videoVariantPlaylistPath = "/video-playlist-${id}"
|
||||
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
|
||||
val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl))
|
||||
val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, videoVariantPlaylistSegments)
|
||||
val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, videoVariantPlaylistSegments)
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
|
@ -514,7 +514,7 @@ class StateCasting {
|
|||
}
|
||||
|
||||
if (audioSource != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castLocalHls")
|
||||
|
@ -523,9 +523,9 @@ class StateCasting {
|
|||
val audioVariantPlaylistPath = "/audio-playlist-${id}"
|
||||
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
|
||||
val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl))
|
||||
val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, audioVariantPlaylistSegments)
|
||||
val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, audioVariantPlaylistSegments)
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
|
@ -535,7 +535,7 @@ class StateCasting {
|
|||
}
|
||||
|
||||
if (subtitleSource != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castLocalHls")
|
||||
|
@ -544,9 +544,9 @@ class StateCasting {
|
|||
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
|
||||
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
|
||||
val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitleUrl))
|
||||
val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, subtitleVariantPlaylistSegments)
|
||||
val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, subtitleVariantPlaylistSegments)
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
|
@ -556,7 +556,7 @@ class StateCasting {
|
|||
}
|
||||
|
||||
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
|
@ -584,43 +584,28 @@ class StateCasting {
|
|||
val audioUrl = url + audioPath;
|
||||
val subtitleUrl = url + subtitlePath;
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
|
||||
"application/dash+xml")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
if (videoSource != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
_castServer.addHandler(
|
||||
HttpOptionsAllowHandler(videoPath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*")
|
||||
.withHeader("Connection", "keep-alive"))
|
||||
.withTag("cast");
|
||||
}
|
||||
if (audioSource != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
_castServer.addHandler(
|
||||
HttpOptionsAllowHandler(audioPath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*")
|
||||
.withHeader("Connection", "keep-alive"))
|
||||
.withTag("cast");
|
||||
}
|
||||
if (subtitleSource != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
_castServer.addHandler(
|
||||
HttpOptionsAllowHandler(subtitlePath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*")
|
||||
.withHeader("Connection", "keep-alive"))
|
||||
.withTag("cast");
|
||||
}
|
||||
|
||||
Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).");
|
||||
|
@ -654,7 +639,7 @@ class StateCasting {
|
|||
}
|
||||
|
||||
if (content != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
|
@ -674,7 +659,7 @@ class StateCasting {
|
|||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
||||
}
|
||||
|
||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, resumePosition: Double): List<String> {
|
||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double): List<String> {
|
||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
@ -685,13 +670,41 @@ class StateCasting {
|
|||
val hlsUrl = url + hlsPath
|
||||
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||
|
||||
_castServer.addHandler(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
||||
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
||||
|
||||
val headers = masterContext.headers.clone()
|
||||
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||
|
||||
val masterPlaylist = HLS.downloadAndParseMasterPlaylist(_client, sourceUrl)
|
||||
val masterPlaylistResponse = _client.get(sourceUrl)
|
||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||
|
||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
?: throw Exception("Master playlist content is empty")
|
||||
|
||||
val masterPlaylist: HLS.MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||
} catch (e: Throwable) {
|
||||
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
//This is a variant playlist, not a master playlist
|
||||
Logger.i(TAG, "HLS casting as variant playlist (codec: $codec): $hlsUrl");
|
||||
|
||||
val vpHeaders = masterContext.headers.clone()
|
||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||
|
||||
val variantPlaylist = HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl)
|
||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||
return@HttpFuntionHandler
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "HLS casting as master playlist: $hlsUrl");
|
||||
|
||||
val newVariantPlaylistRefs = arrayListOf<HLS.VariantPlaylistReference>()
|
||||
val newMediaRenditions = arrayListOf<HLS.MediaRendition>()
|
||||
val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.sessionDataList, masterPlaylist.independentSegments)
|
||||
|
@ -701,11 +714,17 @@ class StateCasting {
|
|||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||
val newPlaylistUrl = url + newPlaylistPath;
|
||||
|
||||
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
val vpHeaders = vpContext.headers.clone()
|
||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||
|
||||
val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, variantPlaylistRef.url)
|
||||
val response = _client.get(variantPlaylistRef.url)
|
||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||
|
||||
val vpContent = response.body?.string()
|
||||
?: throw Exception("Variant playlist content is empty")
|
||||
|
||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url)
|
||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
|
||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||
|
@ -725,11 +744,17 @@ class StateCasting {
|
|||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||
newPlaylistUrl = url + newPlaylistPath
|
||||
|
||||
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||
val vpHeaders = vpContext.headers.clone()
|
||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||
|
||||
val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri)
|
||||
val response = _client.get(mediaRendition.uri)
|
||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||
|
||||
val vpContent = response.body?.string()
|
||||
?: throw Exception("Variant playlist content is empty")
|
||||
|
||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, mediaRendition.uri)
|
||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
|
||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||
|
@ -754,7 +779,7 @@ class StateCasting {
|
|||
Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath).");
|
||||
|
||||
//ChromeCast is sometimes funky with resume position 0
|
||||
val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 1.0 else resumePosition;
|
||||
val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition;
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble());
|
||||
|
||||
return listOf(hlsUrl);
|
||||
|
@ -765,7 +790,7 @@ class StateCasting {
|
|||
|
||||
if (proxySegments) {
|
||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||
val sequenceNumber = variantPlaylist.mediaSequence + index.toLong()
|
||||
val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong()
|
||||
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
||||
}
|
||||
} else {
|
||||
|
@ -779,6 +804,7 @@ class StateCasting {
|
|||
variantPlaylist.discontinuitySequence,
|
||||
variantPlaylist.programDateTime,
|
||||
variantPlaylist.playlistType,
|
||||
variantPlaylist.streamInfo,
|
||||
newSegments
|
||||
)
|
||||
}
|
||||
|
@ -789,7 +815,7 @@ class StateCasting {
|
|||
val newSegmentUrl = url + newSegmentPath;
|
||||
|
||||
if (_castServer.getHandler("GET", newSegmentPath) == null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
|
@ -826,23 +852,21 @@ class StateCasting {
|
|||
val audioVariantPlaylistPath = "/audio-playlist-${id}"
|
||||
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
|
||||
val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl))
|
||||
val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, audioVariantPlaylistSegments)
|
||||
val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, audioVariantPlaylistSegments)
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
_castServer.addHandler(HttpOptionsAllowHandler(audioVariantPlaylistPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
||||
|
||||
mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "en", "english", true, true, true))
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
_castServer.addHandler(HttpOptionsAllowHandler(audioPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
||||
}
|
||||
|
||||
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||
|
@ -861,11 +885,10 @@ class StateCasting {
|
|||
}
|
||||
|
||||
if (content != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
_castServer.addHandler(HttpOptionsAllowHandler(subtitlePath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
||||
}
|
||||
|
||||
subtitlesUrl = url + subtitlePath;
|
||||
|
@ -879,14 +902,13 @@ class StateCasting {
|
|||
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
|
||||
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
|
||||
val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitlesUrl))
|
||||
val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, subtitleVariantPlaylistSegments)
|
||||
val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, subtitleVariantPlaylistSegments)
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
_castServer.addHandler(HttpOptionsAllowHandler(subtitleVariantPlaylistPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
||||
|
||||
mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "en", "english", true, true, true))
|
||||
}
|
||||
|
@ -899,14 +921,13 @@ class StateCasting {
|
|||
val videoVariantPlaylistPath = "/video-playlist-${id}"
|
||||
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
|
||||
val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl))
|
||||
val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, videoVariantPlaylistSegments)
|
||||
val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, videoVariantPlaylistSegments)
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
_castServer.addHandler(HttpOptionsAllowHandler(videoVariantPlaylistPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
||||
|
||||
variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo(
|
||||
videoSource.bitrate ?: 0,
|
||||
|
@ -918,21 +939,19 @@ class StateCasting {
|
|||
if (subtitleSource != null) "subtitles" else null,
|
||||
null, null)))
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
_castServer.addHandler(HttpOptionsAllowHandler(videoPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
||||
}
|
||||
|
||||
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(),
|
||||
"application/vnd.apple.mpegurl")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectMaster")
|
||||
_castServer.addHandler(HttpOptionsAllowHandler(hlsPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
||||
|
||||
Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath).");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble());
|
||||
|
@ -976,11 +995,10 @@ class StateCasting {
|
|||
}
|
||||
|
||||
if (content != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
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;
|
||||
|
@ -989,38 +1007,25 @@ class StateCasting {
|
|||
}
|
||||
}
|
||||
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
|
||||
"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(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
_castServer.addHandler(
|
||||
HttpOptionsAllowHandler(videoPath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*")
|
||||
).withTag("cast");
|
||||
}
|
||||
if (audioSource != null) {
|
||||
_castServer.addHandler(
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
_castServer.addHandler(
|
||||
HttpOptionsAllowHandler(audioPath)
|
||||
.withHeader("Access-Control-Allow-Origin", "*")
|
||||
)
|
||||
.withTag("cast");
|
||||
}
|
||||
|
||||
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||
|
|
|
@ -9,12 +9,7 @@ import java.time.format.DateTimeFormatter
|
|||
|
||||
class HLS {
|
||||
companion object {
|
||||
fun downloadAndParseMasterPlaylist(client: ManagedHttpClient, sourceUrl: String): MasterPlaylist {
|
||||
val masterPlaylistResponse = client.get(sourceUrl)
|
||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||
|
||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
?: throw Exception("Master playlist content is empty")
|
||||
fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String): MasterPlaylist {
|
||||
val baseUrl = URI(sourceUrl).resolve("./").toString()
|
||||
|
||||
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
||||
|
@ -33,7 +28,7 @@ class HLS {
|
|||
}
|
||||
|
||||
line.startsWith("#EXT-X-MEDIA") -> {
|
||||
mediaRenditions.add(parseMediaRendition(client, line, baseUrl))
|
||||
mediaRenditions.add(parseMediaRendition(line, baseUrl))
|
||||
}
|
||||
|
||||
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
||||
|
@ -50,27 +45,21 @@ class HLS {
|
|||
return MasterPlaylist(variantPlaylists, mediaRenditions, sessionDataList, independentSegments)
|
||||
}
|
||||
|
||||
fun downloadAndParseVariantPlaylist(client: ManagedHttpClient, sourceUrl: String): VariantPlaylist {
|
||||
val response = client.get(sourceUrl)
|
||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||
|
||||
val content = response.body?.string()
|
||||
?: throw Exception("Variant playlist content is empty")
|
||||
|
||||
fun parseVariantPlaylist(content: String, sourceUrl: String): VariantPlaylist {
|
||||
val lines = content.lines()
|
||||
val version = lines.find { it.startsWith("#EXT-X-VERSION:") }?.substringAfter(":")?.toIntOrNull() ?: 3
|
||||
val version = lines.find { it.startsWith("#EXT-X-VERSION:") }?.substringAfter(":")?.toIntOrNull()
|
||||
val targetDuration = lines.find { it.startsWith("#EXT-X-TARGETDURATION:") }?.substringAfter(":")?.toIntOrNull()
|
||||
?: throw Exception("Target duration not found in variant playlist")
|
||||
val mediaSequence = lines.find { it.startsWith("#EXT-X-MEDIA-SEQUENCE:") }?.substringAfter(":")?.toLongOrNull() ?: 0
|
||||
val discontinuitySequence = lines.find { it.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:") }?.substringAfter(":")?.toIntOrNull() ?: 0
|
||||
val mediaSequence = lines.find { it.startsWith("#EXT-X-MEDIA-SEQUENCE:") }?.substringAfter(":")?.toLongOrNull()
|
||||
val discontinuitySequence = lines.find { it.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:") }?.substringAfter(":")?.toIntOrNull()
|
||||
val programDateTime = lines.find { it.startsWith("#EXT-X-PROGRAM-DATE-TIME:") }?.substringAfter(":")?.let {
|
||||
ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
|
||||
}
|
||||
val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
|
||||
val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) }
|
||||
|
||||
val segments = mutableListOf<Segment>()
|
||||
var currentSegment: MediaSegment? = null
|
||||
lines.forEach { line ->
|
||||
lines.forEachIndexed { index, line ->
|
||||
when {
|
||||
line.startsWith("#EXTINF:") -> {
|
||||
val duration = line.substringAfter(":").substringBefore(",").toDoubleOrNull()
|
||||
|
@ -93,7 +82,7 @@ class HLS {
|
|||
}
|
||||
}
|
||||
|
||||
return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, segments)
|
||||
return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments)
|
||||
}
|
||||
|
||||
private fun resolveUrl(baseUrl: String, url: String): String {
|
||||
|
@ -123,7 +112,7 @@ class HLS {
|
|||
)
|
||||
}
|
||||
|
||||
private fun parseMediaRendition(client: ManagedHttpClient, line: String, baseUrl: String): MediaRendition {
|
||||
private fun parseMediaRendition(line: String, baseUrl: String): MediaRendition {
|
||||
val attributes = parseAttributes(line)
|
||||
val uri = attributes["URI"]?.let { resolveUrl(baseUrl, it) }
|
||||
return MediaRendition(
|
||||
|
@ -208,7 +197,23 @@ class HLS {
|
|||
val video: String?,
|
||||
val subtitles: String?,
|
||||
val closedCaptions: String?
|
||||
)
|
||||
) {
|
||||
fun toM3U8Line(): String = buildString {
|
||||
append("#EXT-X-STREAM-INF:")
|
||||
appendAttributes(this,
|
||||
"BANDWIDTH" to bandwidth?.toString(),
|
||||
"RESOLUTION" to resolution,
|
||||
"CODECS" to codecs,
|
||||
"FRAME-RATE" to frameRate,
|
||||
"VIDEO-RANGE" to videoRange,
|
||||
"AUDIO" to audio,
|
||||
"VIDEO" to video,
|
||||
"SUBTITLES" to subtitles,
|
||||
"CLOSED-CAPTIONS" to closedCaptions
|
||||
)
|
||||
append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
data class MediaRendition(
|
||||
val type: String?,
|
||||
|
@ -268,45 +273,30 @@ class HLS {
|
|||
|
||||
data class VariantPlaylistReference(val url: String, val streamInfo: StreamInfo) {
|
||||
fun toM3U8Line(): String = buildString {
|
||||
append("#EXT-X-STREAM-INF:")
|
||||
appendAttributes(this,
|
||||
"BANDWIDTH" to streamInfo.bandwidth?.toString(),
|
||||
"RESOLUTION" to streamInfo.resolution,
|
||||
"CODECS" to streamInfo.codecs,
|
||||
"FRAME-RATE" to streamInfo.frameRate,
|
||||
"VIDEO-RANGE" to streamInfo.videoRange,
|
||||
"AUDIO" to streamInfo.audio,
|
||||
"VIDEO" to streamInfo.video,
|
||||
"SUBTITLES" to streamInfo.subtitles,
|
||||
"CLOSED-CAPTIONS" to streamInfo.closedCaptions
|
||||
)
|
||||
append("\n$url\n")
|
||||
append(streamInfo.toM3U8Line())
|
||||
append("$url\n")
|
||||
}
|
||||
}
|
||||
|
||||
data class VariantPlaylist(
|
||||
val version: Int,
|
||||
val targetDuration: Int,
|
||||
val mediaSequence: Long,
|
||||
val discontinuitySequence: Int,
|
||||
val version: Int?,
|
||||
val targetDuration: Int?,
|
||||
val mediaSequence: Long?,
|
||||
val discontinuitySequence: Int?,
|
||||
val programDateTime: ZonedDateTime?,
|
||||
val playlistType: String?,
|
||||
val streamInfo: StreamInfo?,
|
||||
val segments: List<Segment>
|
||||
) {
|
||||
fun buildM3U8(): String = buildString {
|
||||
append("#EXTM3U\n")
|
||||
append("#EXT-X-VERSION:$version\n")
|
||||
append("#EXT-X-TARGETDURATION:$targetDuration\n")
|
||||
append("#EXT-X-MEDIA-SEQUENCE:$mediaSequence\n")
|
||||
append("#EXT-X-DISCONTINUITY-SEQUENCE:$discontinuitySequence\n")
|
||||
|
||||
playlistType?.let {
|
||||
append("#EXT-X-PLAYLIST-TYPE:$it\n")
|
||||
}
|
||||
|
||||
programDateTime?.let {
|
||||
append("#EXT-X-PROGRAM-DATE-TIME:${it.format(DateTimeFormatter.ISO_DATE_TIME)}\n")
|
||||
}
|
||||
version?.let { append("#EXT-X-VERSION:$it\n") }
|
||||
targetDuration?.let { append("#EXT-X-TARGETDURATION:$it\n") }
|
||||
mediaSequence?.let { append("#EXT-X-MEDIA-SEQUENCE:$it\n") }
|
||||
discontinuitySequence?.let { append("#EXT-X-DISCONTINUITY-SEQUENCE:$it\n") }
|
||||
playlistType?.let { append("#EXT-X-PLAYLIST-TYPE:$it\n") }
|
||||
programDateTime?.let { append("#EXT-X-PROGRAM-DATE-TIME:${it.format(DateTimeFormatter.ISO_DATE_TIME)}\n") }
|
||||
streamInfo?.let { append(it.toM3U8Line()) }
|
||||
|
||||
segments.forEach { segment ->
|
||||
append(segment.toM3U8Line())
|
||||
|
|
Loading…
Add table
Reference in a new issue