diff --git a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt index 74dcbb52..b78925a1 100644 --- a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt +++ b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpProxyHandler.kt @@ -5,6 +5,7 @@ import android.util.Log import com.futo.platformplayer.api.http.server.HttpContext import com.futo.platformplayer.api.http.server.HttpHeaders import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.api.media.models.modifier.IRequest import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.parsers.HttpResponseParser import com.futo.platformplayer.readLine @@ -27,6 +28,7 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv private var _injectReferer = false; private val _client = ManagedHttpClient(); + private var _requestModifier: ((String, Map) -> IRequest)? = null; override fun handle(context: HttpContext) { if (useTcp) { @@ -43,21 +45,33 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv for (injectHeader in _injectRequestHeader) proxyHeaders[injectHeader.first] = injectHeader.second; - val parsed = Uri.parse(targetUrl); + val req = _requestModifier?.invoke(targetUrl, proxyHeaders) + var url = targetUrl + if (req != null) { + req.url?.let { + url = it + } + req.headers.let { + proxyHeaders.clear() + proxyHeaders.putAll(it) + } + } + + val parsed = Uri.parse(url); if(_injectHost) proxyHeaders.put("Host", parsed.host!!); if(_injectReferer) - proxyHeaders.put("Referer", targetUrl); + proxyHeaders.put("Referer", url); val useMethod = if (method == "inherit") context.method else method; - Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}"); + Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${url}"); Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n")); val resp = when (useMethod) { - "GET" -> _client.get(targetUrl, proxyHeaders); - "POST" -> _client.post(targetUrl, content ?: "", proxyHeaders); - "HEAD" -> _client.head(targetUrl, proxyHeaders) - else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders); + "GET" -> _client.get(url, proxyHeaders); + "POST" -> _client.post(url, content ?: "", proxyHeaders); + "HEAD" -> _client.head(url, proxyHeaders) + else -> _client.requestMethod(useMethod, url, proxyHeaders); }; Logger.i(TAG, "Proxied Response [${resp.code}]"); @@ -91,11 +105,23 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv for (injectHeader in _injectRequestHeader) proxyHeaders[injectHeader.first] = injectHeader.second; - val parsed = Uri.parse(targetUrl); + val req = _requestModifier?.invoke(targetUrl, proxyHeaders) + var url = targetUrl + if (req != null) { + req.url?.let { + url = it + } + req.headers.let { + proxyHeaders.clear() + proxyHeaders.putAll(it) + } + } + + val parsed = Uri.parse(url); if(_injectHost) proxyHeaders.put("Host", parsed.host!!); if(_injectReferer) - proxyHeaders.put("Referer", targetUrl); + proxyHeaders.put("Referer", url); val useMethod = if (method == "inherit") context.method else method; Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}"); @@ -242,6 +268,10 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv _ignoreRequestHeaders.add("referer"); return this; } + fun withRequestModifier(modifier: (String, Map) -> IRequest) : HttpProxyHandler { + _requestModifier = modifier; + return this; + } companion object { private const val TAG = "HttpProxyHandler" 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 dc66d6b2..269a73b8 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -17,6 +17,7 @@ import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler +import com.futo.platformplayer.api.media.models.modifier.IRequestModifier import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource @@ -295,20 +296,63 @@ abstract class StateCasting { val url = getLocalUrl(ad); val id = UUID.randomUUID(); + if (videoSource is IVideoUrlSource) { - val videoPath = "/video-${id}" - val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl(); - Logger.i(TAG, "Casting as singular video"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video)); + val videoPath = "/video-$id" + val upstreamUrl = videoSource.getVideoUrl() + val videoUrl = if (proxyStreams) url + videoPath else upstreamUrl + val jsReqMod = (videoSource as? JSSource)?.getRequestModifier() + + if (proxyStreams) { + _castServer.addHandlerWithAllowAllOptions( + HttpProxyHandler("GET", videoPath, upstreamUrl, true) + .withIRequestModifier(jsReqMod) + .withInjectedHost() + .withHeader("Access-Control-Allow-Origin", "*"), + true + ).withTag("castSingular") + } + + Logger.i(TAG, "Casting as singular video (proxy=$proxyStreams, url=$videoUrl)") + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + videoSource.container, + videoUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ) } else if (audioSource is IAudioUrlSource) { - val audioPath = "/audio-${id}" - val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl(); - Logger.i(TAG, "Casting as singular audio"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video)); + val audioPath = "/audio-$id" + val upstreamUrl = audioSource.getAudioUrl() + val audioUrl = if (proxyStreams) url + audioPath else upstreamUrl + val jsReqMod = (audioSource as? JSSource)?.getRequestModifier() + + if (proxyStreams) { + _castServer.addHandlerWithAllowAllOptions( + HttpProxyHandler("GET", audioPath, upstreamUrl, true) + .withIRequestModifier(jsReqMod) + .withInjectedHost() + .withHeader("Access-Control-Allow-Origin", "*"), + true + ).withTag("castSingular") + } + + Logger.i(TAG, "Casting as singular audio (proxy=$proxyStreams, url=$audioUrl)") + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + audioSource.container, + audioUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ) } else if (videoSource is IHLSManifestSource) { if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) { Logger.i(TAG, "Casting as proxied HLS"); - castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed); + castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed, (videoSource as JSSource?)?.getRequestModifier()); } 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(), speed, metadataFromVideo(video)); @@ -316,7 +360,7 @@ abstract class StateCasting { } else if (audioSource is IHLSManifestAudioSource) { if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) { Logger.i(TAG, "Casting as proxied audio HLS"); - castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed); + castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed, (audioSource as JSSource?)?.getRequestModifier()); } 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(), speed, metadataFromVideo(video)); @@ -347,6 +391,11 @@ abstract class StateCasting { } } + private fun HttpProxyHandler.withIRequestModifier(requestModifier: IRequestModifier?): HttpProxyHandler { + if (requestModifier == null) return this + return withRequestModifier { url, headers -> requestModifier.modifyRequest(url, headers) } + } + fun resumeVideo(): Boolean { val ad = activeDevice ?: return false; try { @@ -665,7 +714,8 @@ abstract class StateCasting { sourceUrl: String, codec: String?, resumePosition: Double, - speed: Double? + speed: Double?, + requestModifier: IRequestModifier? ): List { _castServer.removeAllHandlers("castProxiedHlsMaster") @@ -686,7 +736,9 @@ abstract class StateCasting { val headers = masterContext.headers.clone() headers["Content-Type"] = "application/vnd.apple.mpegurl"; - val masterPlaylistResponse = _client.get(sourceUrl) + val req = requestModifier?.modifyRequest(sourceUrl, mapOf()) + val masterPlaylistResponse = _client.get(req?.url ?: sourceUrl, (req?.headers ?: mapOf()).toMutableMap()) + check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" } val masterPlaylistContent = masterPlaylistResponse.body?.string() @@ -706,7 +758,7 @@ abstract class StateCasting { val variantPlaylist = HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl) val proxiedVariantPlaylist = - proxyVariantPlaylist(url, id, variantPlaylist, video.isLive) + proxyVariantPlaylist(url, id, variantPlaylist, video.isLive, requestModifier) val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); return@HttpFunctionHandler @@ -747,7 +799,7 @@ abstract class StateCasting { val variantPlaylist = HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url) val proxiedVariantPlaylist = - proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive) + proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive, requestModifier) val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); }.withHeader("Access-Control-Allow-Origin", "*"), true @@ -784,7 +836,7 @@ abstract class StateCasting { val variantPlaylist = HLS.parseVariantPlaylist(vpContent, mediaRendition.uri) val proxiedVariantPlaylist = proxyVariantPlaylist( - url, playlistId, variantPlaylist, video.isLive + url, playlistId, variantPlaylist, video.isLive, requestModifier ) val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); @@ -826,13 +878,13 @@ abstract class StateCasting { return listOf(hlsUrl); } - private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, proxySegments: Boolean = true): HLS.VariantPlaylist { + private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, requestModifier: IRequestModifier?, proxySegments: Boolean = true): HLS.VariantPlaylist { val newSegments = arrayListOf() if (proxySegments) { variantPlaylist.segments.forEachIndexed { index, segment -> val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong() - newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) + newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber, requestModifier)) } } else { newSegments.addAll(variantPlaylist.segments) @@ -850,7 +902,7 @@ abstract class StateCasting { ) } - private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long): HLS.Segment { + private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long, requestModifier: IRequestModifier?): HLS.Segment { if (segment is HLS.MediaSegment) { val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}" val newSegmentUrl = url + newSegmentPath; @@ -858,6 +910,7 @@ abstract class StateCasting { if (_castServer.getHandler("GET", newSegmentPath) == null) { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", newSegmentPath, segment.uri, true) + .withIRequestModifier(requestModifier) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castProxiedHlsVariant")