diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 1002ffb9..f4655b30 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -1241,6 +1241,47 @@ abstract class StateCasting { return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}"; } + private fun escapeXml(s: String): String = + s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + + private fun injectSubtitleAdaptationSet( + mpd: String, + subtitleUrl: String, + mimeType: String, + lang: String = "und", + label: String = "Subtitles" + ): String { + val mt = mimeType.lowercase() + val codecs = when (mt) { + "text/vtt", "text/webvtt" -> "wvtt" + "application/ttml+xml", "application/ttml" -> "stpp" + else -> null + } + val codecsAttr = codecs?.let { " codecs=\"${escapeXml(it)}\"" } ?: "" + + val adaptation = """ + + + + + ${escapeXml(subtitleUrl)} + + + """.trimIndent() + + val periodClose = Regex("", RegexOption.IGNORE_CASE) + + return if (periodClose.containsMatchIn(mpd)) { + mpd.replaceFirst(periodClose, adaptation + "\n") + } else { + mpd + } + } + @OptIn(UnstableApi::class) private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?, castId: Int, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null) : List { val ad = activeDevice ?: return listOf(); @@ -1262,30 +1303,42 @@ abstract class StateCasting { val videoUrl = url + videoPath val audioUrl = url + audioPath + val subtitleMimeTypeFull = subtitleSource?.format ?: "text/vtt" + val subtitleMimeTypeForMpd = subtitleMimeTypeFull.substringBefore(';').trim() + val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) { - return@withContext subtitleSource.getSubtitlesURI(); - } else null; + subtitleSource.getSubtitlesURI() + } else null - var subtitlesUrl: String? = null; + var subtitlesUrl: String? = null if (subtitlesUri != null) { - 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() }; + when (subtitlesUri.scheme) { + "file", "content" -> { + val content = withContext(Dispatchers.IO) { + contentResolver.openInputStream(subtitlesUri)?.use { stream -> + stream.bufferedReader().use { it.readText() } + } + } + + if (!content.isNullOrEmpty()) { + _castServer.addHandlerWithAllowAllOptions( + HttpConstantHandler("GET", subtitlePath, content, subtitleMimeTypeFull) + .withHeader("Access-Control-Allow-Origin", "*"), + true + ).withTag("castDashRaw") + + subtitlesUrl = url + subtitlePath + } } - if (content != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt") - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); + "http", "https" -> { + // Receiver will fetch directly (works only if it doesn’t need auth/headers) + subtitlesUrl = subtitlesUri.toString() } - subtitlesUrl = url + subtitlePath; - } else { - subtitlesUrl = subtitlesUri.toString(); + else -> { + Logger.w(TAG, "Unsupported subtitlesUri scheme: ${subtitlesUri.scheme}") + } } } @@ -1331,6 +1384,14 @@ abstract class StateCasting { return emptyList() } + if (subtitlesUrl != null) { + dashContent = injectSubtitleAdaptationSet( + dashContent, + subtitlesUrl!!, + subtitleMimeTypeForMpd + ) + } + var hasAudioInDash = false for (representation in representationRegex.findAll(dashContent)) { val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")