mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-20 16:27:18 +02:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
f15eb9bf9e
@ -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.exceptions.EmptyRequestException
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
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.HttpHandler
|
||||||
|
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
@ -141,6 +142,23 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
}
|
}
|
||||||
return handler;
|
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) {
|
fun removeHandler(method: String, path: String) {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
val handlerMap = _handlers[method] ?: return
|
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
|
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) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
val newHeaders = headers.clone()
|
val newHeaders = headers.clone()
|
||||||
newHeaders.put("Access-Control-Allow-Origin", "*")
|
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", "*")
|
newHeaders.put("Access-Control-Allow-Headers", "*")
|
||||||
httpContext.respondCode(200, newHeaders);
|
httpContext.respondCode(200, newHeaders);
|
||||||
}
|
}
|
||||||
|
@ -314,6 +314,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
connectionState = CastConnectionState.CONNECTING;
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
_socket?.close()
|
||||||
_socket = factory.createSocket(usedRemoteAddress, port) as SSLSocket;
|
_socket = factory.createSocket(usedRemoteAddress, port) as SSLSocket;
|
||||||
_socket?.startHandshake();
|
_socket?.startHandshake();
|
||||||
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
|
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
|
||||||
@ -324,7 +325,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.i(TAG, "Failed to authenticate to Chromecast.", e);
|
Logger.i(TAG, "Failed to authenticate to Chromecast.", e);
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: Throwable) {
|
||||||
_socket?.close();
|
_socket?.close();
|
||||||
Logger.i(TAG, "Failed to connect to Chromecast.", e);
|
Logger.i(TAG, "Failed to connect to Chromecast.", e);
|
||||||
|
|
||||||
|
@ -370,7 +370,7 @@ class StateCasting {
|
|||||||
} else if(videoSource is IHLSManifestSource) {
|
} else if(videoSource is IHLSManifestSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied HLS");
|
Logger.i(TAG, "Casting as proxied HLS");
|
||||||
castProxiedHls(video, videoSource.url, resumePosition);
|
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition);
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as non-proxied HLS");
|
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());
|
||||||
@ -378,7 +378,7 @@ class StateCasting {
|
|||||||
} else if(audioSource is IHLSManifestAudioSource) {
|
} else if(audioSource is IHLSManifestAudioSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (ad is ChromecastCastingDevice) {
|
||||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||||
castProxiedHls(video, audioSource.url, resumePosition);
|
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition);
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
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());
|
||||||
@ -442,7 +442,7 @@ class StateCasting {
|
|||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
val videoUrl = url + videoPath;
|
val videoUrl = url + videoPath;
|
||||||
|
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@ -461,7 +461,7 @@ class StateCasting {
|
|||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-${id}"
|
||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
|
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@ -492,7 +492,7 @@ class StateCasting {
|
|||||||
val variantPlaylistReferences = arrayListOf<HLS.VariantPlaylistReference>()
|
val variantPlaylistReferences = arrayListOf<HLS.VariantPlaylistReference>()
|
||||||
|
|
||||||
if (videoSource != null) {
|
if (videoSource != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castLocalHls")
|
).withTag("castLocalHls")
|
||||||
@ -501,9 +501,9 @@ class StateCasting {
|
|||||||
val videoVariantPlaylistPath = "/video-playlist-${id}"
|
val videoVariantPlaylistPath = "/video-playlist-${id}"
|
||||||
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
|
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
|
||||||
val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl))
|
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(),
|
HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
@ -514,7 +514,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castLocalHls")
|
).withTag("castLocalHls")
|
||||||
@ -523,9 +523,9 @@ class StateCasting {
|
|||||||
val audioVariantPlaylistPath = "/audio-playlist-${id}"
|
val audioVariantPlaylistPath = "/audio-playlist-${id}"
|
||||||
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
|
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
|
||||||
val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl))
|
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(),
|
HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
@ -535,7 +535,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subtitleSource != null) {
|
if (subtitleSource != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath)
|
HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath)
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castLocalHls")
|
).withTag("castLocalHls")
|
||||||
@ -544,9 +544,9 @@ class StateCasting {
|
|||||||
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
|
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
|
||||||
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
|
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
|
||||||
val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitleUrl))
|
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(),
|
HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
@ -556,7 +556,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(),
|
HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
@ -584,43 +584,28 @@ class StateCasting {
|
|||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
val subtitleUrl = url + subtitlePath;
|
val subtitleUrl = url + subtitlePath;
|
||||||
|
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
|
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
|
||||||
"application/dash+xml")
|
"application/dash+xml")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
if (videoSource != null) {
|
if (videoSource != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath)
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
_castServer.addHandler(
|
|
||||||
HttpOptionsAllowHandler(videoPath)
|
|
||||||
.withHeader("Access-Control-Allow-Origin", "*")
|
|
||||||
.withHeader("Connection", "keep-alive"))
|
|
||||||
.withTag("cast");
|
|
||||||
}
|
}
|
||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath)
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
_castServer.addHandler(
|
|
||||||
HttpOptionsAllowHandler(audioPath)
|
|
||||||
.withHeader("Access-Control-Allow-Origin", "*")
|
|
||||||
.withHeader("Connection", "keep-alive"))
|
|
||||||
.withTag("cast");
|
|
||||||
}
|
}
|
||||||
if (subtitleSource != null) {
|
if (subtitleSource != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath)
|
HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath)
|
||||||
.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", "*")
|
|
||||||
.withHeader("Connection", "keep-alive"))
|
|
||||||
.withTag("cast");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).");
|
Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).");
|
||||||
@ -654,7 +639,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
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");
|
||||||
@ -674,7 +659,7 @@ class StateCasting {
|
|||||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
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")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
@ -685,13 +670,41 @@ class StateCasting {
|
|||||||
val hlsUrl = url + hlsPath
|
val hlsUrl = url + hlsPath
|
||||||
Logger.i(TAG, "HLS url: $hlsUrl");
|
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||||
|
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
||||||
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
||||||
|
|
||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
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 newVariantPlaylistRefs = arrayListOf<HLS.VariantPlaylistReference>()
|
||||||
val newMediaRenditions = arrayListOf<HLS.MediaRendition>()
|
val newMediaRenditions = arrayListOf<HLS.MediaRendition>()
|
||||||
val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.sessionDataList, masterPlaylist.independentSegments)
|
val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.sessionDataList, masterPlaylist.independentSegments)
|
||||||
@ -701,11 +714,17 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
val newPlaylistUrl = url + newPlaylistPath;
|
val newPlaylistUrl = url + newPlaylistPath;
|
||||||
|
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
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 = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
@ -725,11 +744,17 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
newPlaylistUrl = url + newPlaylistPath
|
newPlaylistUrl = url + newPlaylistPath
|
||||||
|
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
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 = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
@ -754,7 +779,7 @@ class StateCasting {
|
|||||||
Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath).");
|
Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath).");
|
||||||
|
|
||||||
//ChromeCast is sometimes funky with resume position 0
|
//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());
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble());
|
||||||
|
|
||||||
return listOf(hlsUrl);
|
return listOf(hlsUrl);
|
||||||
@ -765,7 +790,7 @@ class StateCasting {
|
|||||||
|
|
||||||
if (proxySegments) {
|
if (proxySegments) {
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
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))
|
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -779,6 +804,7 @@ class StateCasting {
|
|||||||
variantPlaylist.discontinuitySequence,
|
variantPlaylist.discontinuitySequence,
|
||||||
variantPlaylist.programDateTime,
|
variantPlaylist.programDateTime,
|
||||||
variantPlaylist.playlistType,
|
variantPlaylist.playlistType,
|
||||||
|
variantPlaylist.streamInfo,
|
||||||
newSegments
|
newSegments
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -789,7 +815,7 @@ class StateCasting {
|
|||||||
val newSegmentUrl = url + newSegmentPath;
|
val newSegmentUrl = url + newSegmentPath;
|
||||||
|
|
||||||
if (_castServer.getHandler("GET", newSegmentPath) == null) {
|
if (_castServer.getHandler("GET", newSegmentPath) == null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
|
HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
@ -826,23 +852,21 @@ class StateCasting {
|
|||||||
val audioVariantPlaylistPath = "/audio-playlist-${id}"
|
val audioVariantPlaylistPath = "/audio-playlist-${id}"
|
||||||
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
|
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
|
||||||
val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl))
|
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(),
|
HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).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))
|
mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "en", "english", true, true, true))
|
||||||
|
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
_castServer.addHandler(HttpOptionsAllowHandler(audioPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||||
@ -861,11 +885,10 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
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("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
_castServer.addHandler(HttpOptionsAllowHandler(subtitlePath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subtitlesUrl = url + subtitlePath;
|
subtitlesUrl = url + subtitlePath;
|
||||||
@ -879,14 +902,13 @@ class StateCasting {
|
|||||||
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
|
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
|
||||||
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
|
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
|
||||||
val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitlesUrl))
|
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(),
|
HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).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))
|
mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "en", "english", true, true, true))
|
||||||
}
|
}
|
||||||
@ -899,14 +921,13 @@ class StateCasting {
|
|||||||
val videoVariantPlaylistPath = "/video-playlist-${id}"
|
val videoVariantPlaylistPath = "/video-playlist-${id}"
|
||||||
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
|
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
|
||||||
val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl))
|
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(),
|
HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
_castServer.addHandler(HttpOptionsAllowHandler(videoVariantPlaylistPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
|
||||||
|
|
||||||
variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo(
|
variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo(
|
||||||
videoSource.bitrate ?: 0,
|
videoSource.bitrate ?: 0,
|
||||||
@ -918,21 +939,19 @@ class StateCasting {
|
|||||||
if (subtitleSource != null) "subtitles" else null,
|
if (subtitleSource != null) "subtitles" else null,
|
||||||
null, null)))
|
null, null)))
|
||||||
|
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
_castServer.addHandler(HttpOptionsAllowHandler(videoPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(),
|
HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(),
|
||||||
"application/vnd.apple.mpegurl")
|
"application/vnd.apple.mpegurl")
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectMaster")
|
).withTag("castHlsIndirectMaster")
|
||||||
_castServer.addHandler(HttpOptionsAllowHandler(hlsPath).withHeader("Access-Control-Allow-Origin", "*")).withTag("castHlsIndirectVariant");
|
|
||||||
|
|
||||||
Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath).");
|
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());
|
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) {
|
if (content != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
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;
|
||||||
@ -989,38 +1007,25 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
|
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
|
||||||
"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.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
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(
|
|
||||||
HttpOptionsAllowHandler(videoPath)
|
|
||||||
.withHeader("Access-Control-Allow-Origin", "*")
|
|
||||||
).withTag("cast");
|
|
||||||
}
|
}
|
||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
_castServer.addHandler(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
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(
|
|
||||||
HttpOptionsAllowHandler(audioPath)
|
|
||||||
.withHeader("Access-Control-Allow-Origin", "*")
|
|
||||||
)
|
|
||||||
.withTag("cast");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||||
|
@ -9,12 +9,7 @@ import java.time.format.DateTimeFormatter
|
|||||||
|
|
||||||
class HLS {
|
class HLS {
|
||||||
companion object {
|
companion object {
|
||||||
fun downloadAndParseMasterPlaylist(client: ManagedHttpClient, sourceUrl: String): MasterPlaylist {
|
fun parseMasterPlaylist(masterPlaylistContent: String, 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")
|
|
||||||
val baseUrl = URI(sourceUrl).resolve("./").toString()
|
val baseUrl = URI(sourceUrl).resolve("./").toString()
|
||||||
|
|
||||||
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
||||||
@ -33,7 +28,7 @@ class HLS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
line.startsWith("#EXT-X-MEDIA") -> {
|
line.startsWith("#EXT-X-MEDIA") -> {
|
||||||
mediaRenditions.add(parseMediaRendition(client, line, baseUrl))
|
mediaRenditions.add(parseMediaRendition(line, baseUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
||||||
@ -50,27 +45,21 @@ class HLS {
|
|||||||
return MasterPlaylist(variantPlaylists, mediaRenditions, sessionDataList, independentSegments)
|
return MasterPlaylist(variantPlaylists, mediaRenditions, sessionDataList, independentSegments)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadAndParseVariantPlaylist(client: ManagedHttpClient, sourceUrl: String): VariantPlaylist {
|
fun parseVariantPlaylist(content: String, 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")
|
|
||||||
|
|
||||||
val lines = content.lines()
|
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()
|
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()
|
||||||
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()
|
||||||
val discontinuitySequence = lines.find { it.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:") }?.substringAfter(":")?.toIntOrNull() ?: 0
|
|
||||||
val programDateTime = lines.find { it.startsWith("#EXT-X-PROGRAM-DATE-TIME:") }?.substringAfter(":")?.let {
|
val programDateTime = lines.find { it.startsWith("#EXT-X-PROGRAM-DATE-TIME:") }?.substringAfter(":")?.let {
|
||||||
ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
|
ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
|
||||||
}
|
}
|
||||||
val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
|
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>()
|
val segments = mutableListOf<Segment>()
|
||||||
var currentSegment: MediaSegment? = null
|
var currentSegment: MediaSegment? = null
|
||||||
lines.forEach { line ->
|
lines.forEachIndexed { index, line ->
|
||||||
when {
|
when {
|
||||||
line.startsWith("#EXTINF:") -> {
|
line.startsWith("#EXTINF:") -> {
|
||||||
val duration = line.substringAfter(":").substringBefore(",").toDoubleOrNull()
|
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 {
|
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 attributes = parseAttributes(line)
|
||||||
val uri = attributes["URI"]?.let { resolveUrl(baseUrl, it) }
|
val uri = attributes["URI"]?.let { resolveUrl(baseUrl, it) }
|
||||||
return MediaRendition(
|
return MediaRendition(
|
||||||
@ -208,7 +197,23 @@ class HLS {
|
|||||||
val video: String?,
|
val video: String?,
|
||||||
val subtitles: String?,
|
val subtitles: String?,
|
||||||
val closedCaptions: 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(
|
data class MediaRendition(
|
||||||
val type: String?,
|
val type: String?,
|
||||||
@ -268,45 +273,30 @@ class HLS {
|
|||||||
|
|
||||||
data class VariantPlaylistReference(val url: String, val streamInfo: StreamInfo) {
|
data class VariantPlaylistReference(val url: String, val streamInfo: StreamInfo) {
|
||||||
fun toM3U8Line(): String = buildString {
|
fun toM3U8Line(): String = buildString {
|
||||||
append("#EXT-X-STREAM-INF:")
|
append(streamInfo.toM3U8Line())
|
||||||
appendAttributes(this,
|
append("$url\n")
|
||||||
"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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class VariantPlaylist(
|
data class VariantPlaylist(
|
||||||
val version: Int,
|
val version: Int?,
|
||||||
val targetDuration: Int,
|
val targetDuration: Int?,
|
||||||
val mediaSequence: Long,
|
val mediaSequence: Long?,
|
||||||
val discontinuitySequence: Int,
|
val discontinuitySequence: Int?,
|
||||||
val programDateTime: ZonedDateTime?,
|
val programDateTime: ZonedDateTime?,
|
||||||
val playlistType: String?,
|
val playlistType: String?,
|
||||||
|
val streamInfo: StreamInfo?,
|
||||||
val segments: List<Segment>
|
val segments: List<Segment>
|
||||||
) {
|
) {
|
||||||
fun buildM3U8(): String = buildString {
|
fun buildM3U8(): String = buildString {
|
||||||
append("#EXTM3U\n")
|
append("#EXTM3U\n")
|
||||||
append("#EXT-X-VERSION:$version\n")
|
version?.let { append("#EXT-X-VERSION:$it\n") }
|
||||||
append("#EXT-X-TARGETDURATION:$targetDuration\n")
|
targetDuration?.let { append("#EXT-X-TARGETDURATION:$it\n") }
|
||||||
append("#EXT-X-MEDIA-SEQUENCE:$mediaSequence\n")
|
mediaSequence?.let { append("#EXT-X-MEDIA-SEQUENCE:$it\n") }
|
||||||
append("#EXT-X-DISCONTINUITY-SEQUENCE:$discontinuitySequence\n")
|
discontinuitySequence?.let { append("#EXT-X-DISCONTINUITY-SEQUENCE:$it\n") }
|
||||||
|
playlistType?.let { append("#EXT-X-PLAYLIST-TYPE:$it\n") }
|
||||||
playlistType?.let {
|
programDateTime?.let { append("#EXT-X-PROGRAM-DATE-TIME:${it.format(DateTimeFormatter.ISO_DATE_TIME)}\n") }
|
||||||
append("#EXT-X-PLAYLIST-TYPE:$it\n")
|
streamInfo?.let { append(it.toM3U8Line()) }
|
||||||
}
|
|
||||||
|
|
||||||
programDateTime?.let {
|
|
||||||
append("#EXT-X-PROGRAM-DATE-TIME:${it.format(DateTimeFormatter.ISO_DATE_TIME)}\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.forEach { segment ->
|
segments.forEach { segment ->
|
||||||
append(segment.toM3U8Line())
|
append(segment.toM3U8Line())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user