mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
@@ -101,7 +101,7 @@ fun String.isHexColor(): Boolean {
|
|||||||
|
|
||||||
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
|
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
|
||||||
|
|
||||||
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
||||||
|
|
||||||
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
||||||
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.withStateAtLeast
|
import androidx.lifecycle.withStateAtLeast
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.curlbind.Libcurl
|
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.RootInsetsController
|
import com.futo.platformplayer.RootInsetsController
|
||||||
|
|||||||
+39
-9
@@ -5,6 +5,7 @@ import android.util.Log
|
|||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
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.logging.Logger
|
||||||
import com.futo.platformplayer.parsers.HttpResponseParser
|
import com.futo.platformplayer.parsers.HttpResponseParser
|
||||||
import com.futo.platformplayer.readLine
|
import com.futo.platformplayer.readLine
|
||||||
@@ -27,6 +28,7 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
private var _injectReferer = false;
|
private var _injectReferer = false;
|
||||||
|
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
|
private var _requestModifier: ((String, Map<String, String>) -> IRequest)? = null;
|
||||||
|
|
||||||
override fun handle(context: HttpContext) {
|
override fun handle(context: HttpContext) {
|
||||||
if (useTcp) {
|
if (useTcp) {
|
||||||
@@ -43,21 +45,33 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
for (injectHeader in _injectRequestHeader)
|
for (injectHeader in _injectRequestHeader)
|
||||||
proxyHeaders[injectHeader.first] = injectHeader.second;
|
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)
|
if(_injectHost)
|
||||||
proxyHeaders.put("Host", parsed.host!!);
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
if(_injectReferer)
|
if(_injectReferer)
|
||||||
proxyHeaders.put("Referer", targetUrl);
|
proxyHeaders.put("Referer", url);
|
||||||
|
|
||||||
val useMethod = if (method == "inherit") context.method else method;
|
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"));
|
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
||||||
|
|
||||||
val resp = when (useMethod) {
|
val resp = when (useMethod) {
|
||||||
"GET" -> _client.get(targetUrl, proxyHeaders);
|
"GET" -> _client.get(url, proxyHeaders);
|
||||||
"POST" -> _client.post(targetUrl, content ?: "", proxyHeaders);
|
"POST" -> _client.post(url, content ?: "", proxyHeaders);
|
||||||
"HEAD" -> _client.head(targetUrl, proxyHeaders)
|
"HEAD" -> _client.head(url, proxyHeaders)
|
||||||
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
|
else -> _client.requestMethod(useMethod, url, proxyHeaders);
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.i(TAG, "Proxied Response [${resp.code}]");
|
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)
|
for (injectHeader in _injectRequestHeader)
|
||||||
proxyHeaders[injectHeader.first] = injectHeader.second;
|
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)
|
if(_injectHost)
|
||||||
proxyHeaders.put("Host", parsed.host!!);
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
if(_injectReferer)
|
if(_injectReferer)
|
||||||
proxyHeaders.put("Referer", targetUrl);
|
proxyHeaders.put("Referer", url);
|
||||||
|
|
||||||
val useMethod = if (method == "inherit") context.method else method;
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
|
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");
|
_ignoreRequestHeaders.add("referer");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
fun withRequestModifier(modifier: (String, Map<String, String>) -> IRequest) : HttpProxyHandler {
|
||||||
|
_requestModifier = modifier;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "HttpProxyHandler"
|
private const val TAG = "HttpProxyHandler"
|
||||||
|
|||||||
@@ -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.HttpFileHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
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.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||||
@@ -295,20 +296,63 @@ abstract class StateCasting {
|
|||||||
val url = getLocalUrl(ad);
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-$id"
|
||||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
val upstreamUrl = videoSource.getVideoUrl()
|
||||||
Logger.i(TAG, "Casting as singular video");
|
val videoUrl = if (proxyStreams) url + videoPath else upstreamUrl
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
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) {
|
} else if (audioSource is IAudioUrlSource) {
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-$id"
|
||||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
|
val upstreamUrl = audioSource.getAudioUrl()
|
||||||
Logger.i(TAG, "Casting as singular audio");
|
val audioUrl = if (proxyStreams) url + audioPath else upstreamUrl
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
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) {
|
} else if (videoSource is IHLSManifestSource) {
|
||||||
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
||||||
Logger.i(TAG, "Casting as proxied HLS");
|
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 {
|
} 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(), speed, metadataFromVideo(video));
|
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) {
|
} else if (audioSource is IHLSManifestAudioSource) {
|
||||||
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
||||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
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 {
|
} 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(), speed, metadataFromVideo(video));
|
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 {
|
fun resumeVideo(): Boolean {
|
||||||
val ad = activeDevice ?: return false;
|
val ad = activeDevice ?: return false;
|
||||||
try {
|
try {
|
||||||
@@ -665,7 +714,8 @@ abstract class StateCasting {
|
|||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
codec: String?,
|
codec: String?,
|
||||||
resumePosition: Double,
|
resumePosition: Double,
|
||||||
speed: Double?
|
speed: Double?,
|
||||||
|
requestModifier: IRequestModifier?
|
||||||
): List<String> {
|
): List<String> {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
@@ -686,7 +736,9 @@ abstract class StateCasting {
|
|||||||
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 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}" }
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
@@ -706,7 +758,7 @@ abstract class StateCasting {
|
|||||||
val variantPlaylist =
|
val variantPlaylist =
|
||||||
HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl)
|
HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
val proxiedVariantPlaylist =
|
val proxiedVariantPlaylist =
|
||||||
proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
proxyVariantPlaylist(url, id, variantPlaylist, video.isLive, requestModifier)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
return@HttpFunctionHandler
|
return@HttpFunctionHandler
|
||||||
@@ -747,7 +799,7 @@ abstract class StateCasting {
|
|||||||
val variantPlaylist =
|
val variantPlaylist =
|
||||||
HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url)
|
HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url)
|
||||||
val proxiedVariantPlaylist =
|
val proxiedVariantPlaylist =
|
||||||
proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
|
proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive, requestModifier)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
@@ -784,7 +836,7 @@ abstract class StateCasting {
|
|||||||
val variantPlaylist =
|
val variantPlaylist =
|
||||||
HLS.parseVariantPlaylist(vpContent, mediaRendition.uri)
|
HLS.parseVariantPlaylist(vpContent, mediaRendition.uri)
|
||||||
val proxiedVariantPlaylist = proxyVariantPlaylist(
|
val proxiedVariantPlaylist = proxyVariantPlaylist(
|
||||||
url, playlistId, variantPlaylist, video.isLive
|
url, playlistId, variantPlaylist, video.isLive, requestModifier
|
||||||
)
|
)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
@@ -826,13 +878,13 @@ abstract class StateCasting {
|
|||||||
return listOf(hlsUrl);
|
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<HLS.Segment>()
|
val newSegments = arrayListOf<HLS.Segment>()
|
||||||
|
|
||||||
if (proxySegments) {
|
if (proxySegments) {
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||||
val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong()
|
val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong()
|
||||||
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber, requestModifier))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newSegments.addAll(variantPlaylist.segments)
|
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) {
|
if (segment is HLS.MediaSegment) {
|
||||||
val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}"
|
val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}"
|
||||||
val newSegmentUrl = url + newSegmentPath;
|
val newSegmentUrl = url + newSegmentPath;
|
||||||
@@ -858,6 +910,7 @@ abstract class StateCasting {
|
|||||||
if (_castServer.getHandler("GET", newSegmentPath) == null) {
|
if (_castServer.getHandler("GET", newSegmentPath) == null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
|
HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
|
||||||
|
.withIRequestModifier(requestModifier)
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castProxiedHlsVariant")
|
).withTag("castProxiedHlsVariant")
|
||||||
|
|||||||
+15
-3
@@ -26,6 +26,7 @@ import com.futo.platformplayer.models.HistoryVideo
|
|||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.views.ToggleBar
|
import com.futo.platformplayer.views.ToggleBar
|
||||||
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
|
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
|
||||||
@@ -243,12 +244,23 @@ class HistoryFragment : MainFragment() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
|
||||||
val diff = v.video.duration - v.position;
|
val diff = v.video.duration - v.position;
|
||||||
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
|
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
|
||||||
StatePlayer.instance.clearQueue();
|
|
||||||
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
|
val playlistId = v.playlistId
|
||||||
|
val playlist = playlistId?.let { StatePlaylists.instance.getPlaylist(it) }
|
||||||
|
val playlistIndex = playlist?.videos?.indexOfFirst { it.url == v.video.url }
|
||||||
|
if (playlist != null && playlistIndex != null && playlistIndex >= 0) {
|
||||||
|
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, playlistIndex)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
StatePlayer.instance.clearQueue();
|
||||||
|
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
|
||||||
|
}
|
||||||
|
|
||||||
_editSearch.clearFocus();
|
_editSearch.clearFocus();
|
||||||
|
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
|
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
|
||||||
|
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
|||||||
+3
-2
@@ -1777,7 +1777,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
false,
|
false,
|
||||||
(toResume.toFloat() / 1000.0f).toLong(),
|
(toResume.toFloat() / 1000.0f).toLong(),
|
||||||
null,
|
null,
|
||||||
true
|
true,
|
||||||
|
StatePlayer.instance.playlistId
|
||||||
);
|
);
|
||||||
Logger.i(
|
Logger.i(
|
||||||
TAG,
|
TAG,
|
||||||
@@ -3095,7 +3096,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (v !is TutorialFragment.TutorialVideo) {
|
if (v !is TutorialFragment.TutorialVideo) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val history = getHistoryIndex(v) ?: return@launch;
|
val history = getHistoryIndex(v) ?: return@launch;
|
||||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong(), null, true);
|
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong(), null, true, StatePlayer.instance.playlistId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastPositionSaveTime = currentTime;
|
_lastPositionSaveTime = currentTime;
|
||||||
|
|||||||
@@ -14,15 +14,17 @@ import java.time.ZoneOffset
|
|||||||
class HistoryVideo {
|
class HistoryVideo {
|
||||||
var video: SerializedPlatformVideo;
|
var video: SerializedPlatformVideo;
|
||||||
var position: Long;
|
var position: Long;
|
||||||
|
var playlistId: String? = null
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var date: OffsetDateTime;
|
var date: OffsetDateTime;
|
||||||
|
|
||||||
|
|
||||||
constructor(video: SerializedPlatformVideo, position: Long, date: OffsetDateTime) {
|
constructor(video: SerializedPlatformVideo, position: Long, date: OffsetDateTime, playlistId: String?) {
|
||||||
this.video = video;
|
this.video = video;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
|
this.playlistId = playlistId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ class HistoryVideo {
|
|||||||
viewCount = -1
|
viewCount = -1
|
||||||
);
|
);
|
||||||
|
|
||||||
return HistoryVideo(video, position, OffsetDateTime.of(LocalDateTime.ofEpochSecond(dateSec, 0, ZoneOffset.UTC), ZoneOffset.UTC));
|
return HistoryVideo(video, position, OffsetDateTime.of(LocalDateTime.ofEpochSecond(dateSec, 0, ZoneOffset.UTC), ZoneOffset.UTC), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ class StateBackup {
|
|||||||
}
|
}
|
||||||
val hist = StateHistory.instance.getHistoryByVideo(histObj.video, true, histObj.date);
|
val hist = StateHistory.instance.getHistoryByVideo(histObj.video, true, histObj.date);
|
||||||
if(hist != null)
|
if(hist != null)
|
||||||
StateHistory.instance.updateHistoryPosition(histObj.video, hist, true, histObj.position, histObj.date, false);
|
StateHistory.instance.updateHistoryPosition(histObj.video, hist, true, histObj.position, histObj.date, false, histObj.playlistId);
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to import subscription group", ex);
|
Logger.e(TAG, "Failed to import subscription group", ex);
|
||||||
|
|||||||
@@ -543,7 +543,9 @@ class StateDownloads {
|
|||||||
val file = export.export(context, { progress ->
|
val file = export.export(context, { progress ->
|
||||||
val now = System.currentTimeMillis();
|
val now = System.currentTimeMillis();
|
||||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||||
it.setProgress(progress);
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
it.setProgress(progress);
|
||||||
|
}
|
||||||
lastNotifyTime = now;
|
lastNotifyTime = now;
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class StateHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var _lastHistoryBroadcast = "";
|
private var _lastHistoryBroadcast = "";
|
||||||
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L, date: OffsetDateTime? = null, isUserAction: Boolean = false): Long {
|
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L, date: OffsetDateTime? = null, isUserAction: Boolean = false, playlistId: String? = null): Long {
|
||||||
val pos = if(position < 0) 0 else position;
|
val pos = if(position < 0) 0 else position;
|
||||||
val historyVideo = index.obj;
|
val historyVideo = index.obj;
|
||||||
|
|
||||||
@@ -86,6 +86,7 @@ class StateHistory {
|
|||||||
|
|
||||||
historyVideo.position = pos;
|
historyVideo.position = pos;
|
||||||
historyVideo.date = date ?: OffsetDateTime.now();
|
historyVideo.date = date ?: OffsetDateTime.now();
|
||||||
|
historyVideo.playlistId = playlistId
|
||||||
_historyDBStore.update(index.id!!, historyVideo);
|
_historyDBStore.update(index.id!!, historyVideo);
|
||||||
onHistoricVideoChanged.emit(liveObj, pos);
|
onHistoricVideoChanged.emit(liveObj, pos);
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ class StateHistory {
|
|||||||
UIDialogs.toast("History item null?\nNo history tracking..");
|
UIDialogs.toast("History item null?\nNo history tracking..");
|
||||||
}
|
}
|
||||||
else if(create) {
|
else if(create) {
|
||||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now(), StatePlayer.instance.playlistId);
|
||||||
val id = _historyDBStore.insert(newHistItem);
|
val id = _historyDBStore.insert(newHistItem);
|
||||||
result = _historyDBStore.getOrNull(id);
|
result = _historyDBStore.getOrNull(id);
|
||||||
if(result == null)
|
if(result == null)
|
||||||
|
|||||||
@@ -114,6 +114,15 @@ class StatePlayer {
|
|||||||
var currentVideo: IPlatformVideo? = null
|
var currentVideo: IPlatformVideo? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
|
private var _currentPlaylistId: String? = null
|
||||||
|
val playlistId: String? get() = if (_queueType == TYPE_PLAYLIST) _currentPlaylistId else null
|
||||||
|
|
||||||
|
init {
|
||||||
|
onQueueChanged.subscribe {
|
||||||
|
updateLastQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setCurrentlyPlaying(video: IPlatformVideo?) {
|
fun setCurrentlyPlaying(video: IPlatformVideo?) {
|
||||||
currentVideo = video;
|
currentVideo = video;
|
||||||
}
|
}
|
||||||
@@ -269,23 +278,6 @@ class StatePlayer {
|
|||||||
}
|
}
|
||||||
onQueueChanged.emit(true);
|
onQueueChanged.emit(true);
|
||||||
}
|
}
|
||||||
fun setPlaylist(playlist: IPlatformPlaylistDetails, toPlayIndex: Int = 0, focus: Boolean = false, shuffle: Boolean = false) {
|
|
||||||
synchronized(_queue) {
|
|
||||||
_queue.clear();
|
|
||||||
setQueueType(TYPE_PLAYLIST);
|
|
||||||
_queueName = playlist.name;
|
|
||||||
_queue.addAll(playlist.contents.getResults());
|
|
||||||
queueFocused = focus;
|
|
||||||
queueShuffle = shuffle;
|
|
||||||
if (shuffle) {
|
|
||||||
createShuffledQueue();
|
|
||||||
}
|
|
||||||
_queuePosition = toPlayIndex;
|
|
||||||
}
|
|
||||||
playlist.id.value?.let { StatePlaylists.instance.didPlay(it); };
|
|
||||||
|
|
||||||
onQueueChanged.emit(true);
|
|
||||||
}
|
|
||||||
fun setPlaylist(playlist: Playlist, toPlayIndex: Int = 0, focus: Boolean = false, shuffle: Boolean = false) {
|
fun setPlaylist(playlist: Playlist, toPlayIndex: Int = 0, focus: Boolean = false, shuffle: Boolean = false) {
|
||||||
synchronized(_queue) {
|
synchronized(_queue) {
|
||||||
_queue.clear();
|
_queue.clear();
|
||||||
@@ -299,6 +291,7 @@ class StatePlayer {
|
|||||||
}
|
}
|
||||||
_queuePosition = toPlayIndex;
|
_queuePosition = toPlayIndex;
|
||||||
}
|
}
|
||||||
|
_currentPlaylistId = playlist.id
|
||||||
StatePlaylists.instance.didPlay(playlist.id);
|
StatePlaylists.instance.didPlay(playlist.id);
|
||||||
|
|
||||||
onQueueChanged.emit(true);
|
onQueueChanged.emit(true);
|
||||||
@@ -384,6 +377,23 @@ class StatePlayer {
|
|||||||
setQueuePosition(video);
|
setQueuePosition(video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateLastQueue() {
|
||||||
|
val queueVideos = synchronized(_queue) {
|
||||||
|
if (!_queue.isEmpty()) {
|
||||||
|
return@synchronized _queue.map { SerializedPlatformVideo.fromVideo(it) }.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return@synchronized null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueVideos != null) {
|
||||||
|
val playlist = StatePlaylists.instance.getPlaylist(StatePlaylists.LAST_QUEUE_PLAYLIST_ID) ?: Playlist("Last Queue", queueVideos).apply {
|
||||||
|
id = StatePlaylists.LAST_QUEUE_PLAYLIST_ID
|
||||||
|
}
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
fun setQueuePosition(video: IPlatformVideo) {
|
fun setQueuePosition(video: IPlatformVideo) {
|
||||||
synchronized(_queue) {
|
synchronized(_queue) {
|
||||||
if (getCurrentQueueItem() == video) {
|
if (getCurrentQueueItem() == video) {
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ class StatePlaylists {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLastPlayedPlaylist() : Playlist? {
|
fun getLastPlayedPlaylist() : Playlist? {
|
||||||
return playlistStore.queryItem { it.maxByOrNull { x -> x.datePlayed } };
|
return playlistStore.queryItem { it.filter { x -> x.id != StatePlaylists.LAST_QUEUE_PLAYLIST_ID }.maxByOrNull { x -> x.datePlayed } };
|
||||||
}
|
}
|
||||||
fun getLastUpdatedPlaylist() : Playlist? {
|
fun getLastUpdatedPlaylist() : Playlist? {
|
||||||
return playlistStore.queryItem { it.maxByOrNull { x -> x.dateUpdate } };
|
return playlistStore.queryItem { it.filter { x -> x.id != StatePlaylists.LAST_QUEUE_PLAYLIST_ID }.maxByOrNull { x -> x.dateUpdate } };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPlaylists() : List<Playlist> {
|
fun getPlaylists() : List<Playlist> {
|
||||||
@@ -394,6 +394,7 @@ class StatePlaylists {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "StatePlaylists";
|
val TAG = "StatePlaylists";
|
||||||
|
val LAST_QUEUE_PLAYLIST_ID = "a70a3287-45dd-4227-832c-6ecde7fb1bf6"
|
||||||
private var _instance : StatePlaylists? = null;
|
private var _instance : StatePlaylists? = null;
|
||||||
private var _lockObject = Object()
|
private var _lockObject = Object()
|
||||||
val instance : StatePlaylists
|
val instance : StatePlaylists
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ class StateSync {
|
|||||||
for(video in history){
|
for(video in history){
|
||||||
val hist = StateHistory.instance.getHistoryByVideo(video.video, true, video.date);
|
val hist = StateHistory.instance.getHistoryByVideo(video.video, true, video.date);
|
||||||
if(hist != null)
|
if(hist != null)
|
||||||
StateHistory.instance.updateHistoryPosition(video.video, hist, true, video.position, video.date)
|
StateHistory.instance.updateHistoryPosition(video.video, hist, true, video.position, video.date, false, video.playlistId)
|
||||||
if(lastHistory < video.date)
|
if(lastHistory < video.date)
|
||||||
lastHistory = video.date;
|
lastHistory = video.date;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user