mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-29 13:00:21 +02:00
Extended HLS spec, fixes to YES NO booleans, started on implementing HLS stream combiner.
This commit is contained in:
parent
9d5888ddf7
commit
e4c89e9aa9
@ -21,4 +21,8 @@ inline fun <reified T, R> Any.assume(cb: (T) -> R): R? {
|
|||||||
|
|
||||||
fun String?.yesNoToBoolean(): Boolean {
|
fun String?.yesNoToBoolean(): Boolean {
|
||||||
return this?.uppercase() == "YES"
|
return this?.uppercase() == "YES"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Boolean?.toYesNo(): String {
|
||||||
|
return if (this == true) "YES" else "NO"
|
||||||
}
|
}
|
@ -365,7 +365,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");
|
||||||
castHlsIndirect(video, videoSource.url, resumePosition);
|
castProxiedHls(video, videoSource.url, 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());
|
||||||
@ -373,7 +373,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");
|
||||||
castHlsIndirect(video, audioSource.url, resumePosition);
|
castProxiedHls(video, audioSource.url, 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());
|
||||||
@ -574,7 +574,7 @@ class StateCasting {
|
|||||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castHlsIndirect(video: IPlatformVideoDetails, sourceUrl: String, resumePosition: Double): List<String> {
|
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, resumePosition: Double): List<String> {
|
||||||
_castServer.removeAllHandlers("castHlsIndirectMaster")
|
_castServer.removeAllHandlers("castHlsIndirectMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
@ -695,6 +695,138 @@ class StateCasting {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
|
||||||
|
val ad = activeDevice ?: return listOf();
|
||||||
|
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
|
val hlsPath = "/hls-${id}"
|
||||||
|
|
||||||
|
val hlsUrl = url + hlsPath;
|
||||||
|
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||||
|
|
||||||
|
val mediaRenditions = arrayListOf<HLS.MediaRendition>()
|
||||||
|
val variantPlaylistReferences = arrayListOf<HLS.VariantPlaylistReference>()
|
||||||
|
|
||||||
|
if (audioSource != null) {
|
||||||
|
val audioPath = "/audio-${id}"
|
||||||
|
val audioUrl = url + audioPath
|
||||||
|
|
||||||
|
val duration = audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown")
|
||||||
|
val audioVariantPlaylistPath = "/audio-playlist-${id}"
|
||||||
|
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
|
||||||
|
val audioVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), audioUrl))
|
||||||
|
val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, audioVariantPlaylistSegments)
|
||||||
|
|
||||||
|
_castServer.addHandler(
|
||||||
|
HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(),
|
||||||
|
"application/vnd.apple.mpegurl")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castHlsIndirectVariant");
|
||||||
|
|
||||||
|
mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "en", "english", true, true, true))
|
||||||
|
|
||||||
|
_castServer.addHandler(
|
||||||
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withInjectedHost()
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||||
|
return@withContext subtitleSource.getSubtitlesURI();
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
var subtitlesUrl: String? = null;
|
||||||
|
if (subtitlesUri != null) {
|
||||||
|
val subtitlePath = "/subtitles-${id}"
|
||||||
|
if(subtitlesUri.scheme == "file") {
|
||||||
|
var content: String? = null;
|
||||||
|
val inputStream = contentResolver.openInputStream(subtitlesUri);
|
||||||
|
inputStream?.use { stream ->
|
||||||
|
val reader = stream.bufferedReader();
|
||||||
|
content = reader.use { it.readText() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
_castServer.addHandler(
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
subtitlesUrl = subtitlesUri.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subtitlesUrl != null) {
|
||||||
|
val duration = videoSource?.duration ?: audioSource?.duration ?: throw Exception("Duration unknown")
|
||||||
|
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
|
||||||
|
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
|
||||||
|
val subtitleVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), subtitlesUrl))
|
||||||
|
val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, subtitleVariantPlaylistSegments)
|
||||||
|
|
||||||
|
_castServer.addHandler(
|
||||||
|
HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(),
|
||||||
|
"application/vnd.apple.mpegurl")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castHlsIndirectVariant");
|
||||||
|
|
||||||
|
mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "en", "english", true, true, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSource != null) {
|
||||||
|
val videoPath = "/video-${id}"
|
||||||
|
val videoUrl = url + videoPath
|
||||||
|
|
||||||
|
val duration = videoSource.duration
|
||||||
|
val videoVariantPlaylistPath = "/video-playlist-${id}"
|
||||||
|
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
|
||||||
|
val videoVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), videoUrl))
|
||||||
|
val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, videoVariantPlaylistSegments)
|
||||||
|
|
||||||
|
_castServer.addHandler(
|
||||||
|
HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(),
|
||||||
|
"application/vnd.apple.mpegurl")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castHlsIndirectVariant");
|
||||||
|
|
||||||
|
variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo(
|
||||||
|
videoSource.bitrate ?: 0,
|
||||||
|
"${videoSource.width}x${videoSource.height}",
|
||||||
|
videoSource.codec,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
if (audioSource != null) "audio" else null,
|
||||||
|
if (subtitleSource != null) "subtitles" else null,
|
||||||
|
null)))
|
||||||
|
|
||||||
|
_castServer.addHandler(
|
||||||
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withInjectedHost()
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
||||||
|
_castServer.addHandler(
|
||||||
|
HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(),
|
||||||
|
"application/vnd.apple.mpegurl")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castHlsIndirectMaster")
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
|
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = ad !is FastCastCastingDevice;
|
val proxyStreams = ad !is FastCastCastingDevice;
|
||||||
@ -782,7 +914,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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).");
|
||||||
ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble());
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble());
|
||||||
|
|
||||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.parsers
|
package com.futo.platformplayer.parsers
|
||||||
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.toYesNo
|
||||||
import com.futo.platformplayer.yesNoToBoolean
|
import com.futo.platformplayer.yesNoToBoolean
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
@ -112,6 +113,7 @@ class HLS {
|
|||||||
frameRate = attributes["FRAME-RATE"],
|
frameRate = attributes["FRAME-RATE"],
|
||||||
videoRange = attributes["VIDEO-RANGE"],
|
videoRange = attributes["VIDEO-RANGE"],
|
||||||
audio = attributes["AUDIO"],
|
audio = attributes["AUDIO"],
|
||||||
|
subtitles = attributes["SUBTITLES"],
|
||||||
closedCaptions = attributes["CLOSED-CAPTIONS"]
|
closedCaptions = attributes["CLOSED-CAPTIONS"]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -198,6 +200,7 @@ class HLS {
|
|||||||
val frameRate: String?,
|
val frameRate: String?,
|
||||||
val videoRange: String?,
|
val videoRange: String?,
|
||||||
val audio: String?,
|
val audio: String?,
|
||||||
|
val subtitles: String?,
|
||||||
val closedCaptions: String?
|
val closedCaptions: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -219,9 +222,9 @@ class HLS {
|
|||||||
"GROUP-ID" to groupID,
|
"GROUP-ID" to groupID,
|
||||||
"LANGUAGE" to language,
|
"LANGUAGE" to language,
|
||||||
"NAME" to name,
|
"NAME" to name,
|
||||||
"DEFAULT" to isDefault?.toString()?.uppercase(),
|
"DEFAULT" to isDefault.toYesNo(),
|
||||||
"AUTOSELECT" to isAutoSelect?.toString()?.uppercase(),
|
"AUTOSELECT" to isAutoSelect.toYesNo(),
|
||||||
"FORCED" to isForced?.toString()?.uppercase()
|
"FORCED" to isForced.toYesNo()
|
||||||
)
|
)
|
||||||
append("\n")
|
append("\n")
|
||||||
}
|
}
|
||||||
@ -267,6 +270,7 @@ class HLS {
|
|||||||
"FRAME-RATE" to streamInfo.frameRate,
|
"FRAME-RATE" to streamInfo.frameRate,
|
||||||
"VIDEO-RANGE" to streamInfo.videoRange,
|
"VIDEO-RANGE" to streamInfo.videoRange,
|
||||||
"AUDIO" to streamInfo.audio,
|
"AUDIO" to streamInfo.audio,
|
||||||
|
"SUBTITLES" to streamInfo.subtitles,
|
||||||
"CLOSED-CAPTIONS" to streamInfo.closedCaptions
|
"CLOSED-CAPTIONS" to streamInfo.closedCaptions
|
||||||
)
|
)
|
||||||
append("\n$url\n")
|
append("\n$url\n")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user