Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into shorts-tab

This commit is contained in:
Koen J 2025-07-11 10:16:21 +02:00
commit 3310ac6008
41 changed files with 791 additions and 246 deletions

View file

@ -238,6 +238,9 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
else else
V8Deferred<T>(underlyingDef); V8Deferred<T>(underlyingDef);
if(def.estDuration > 0)
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
val promise = this; val promise = this;
plugin.busy { plugin.busy {
this.register(object: IV8ValuePromise.IListener { this.register(object: IV8ValuePromise.IListener {
@ -294,17 +297,32 @@ class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Defer
} }
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any): T { fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
var result = this.invoke<V8Value>(method, *obj); var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) { if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!); return result.toV8ValueBlocking(this.getSourcePlugin()!!);
} }
return result as T; return result as T;
} }
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any): V8Deferred<T> { fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj); var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) { if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!); return result.toV8ValueAsync(this.getSourcePlugin()!!);
} }
return V8Deferred(CompletableDeferred(result as T)); return V8Deferred(CompletableDeferred(result as T));
}
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result;
}
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
return result;
}
return V8Deferred(CompletableDeferred(result));
} }

View file

@ -32,7 +32,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStateAtLeast
import androidx.lifecycle.withStateAtLeast import androidx.lifecycle.withStateAtLeast
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.BuildConfig
@ -115,7 +114,6 @@ import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.LinkedList import java.util.LinkedList
import java.util.Queue
import java.util.UUID import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -613,6 +611,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
) )
} }
//startActivity(Intent(this, TestActivity::class.java))
} }
/* /*

View file

@ -2,12 +2,24 @@ package com.futo.platformplayer.activities
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.views.TargetTapLoaderView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class TestActivity : AppCompatActivity() { class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test); setContentView(R.layout.activity_test);
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
view.startLoader(10000)
lifecycleScope.launch {
delay(5000)
view.startLoader()
}
} }
companion object { companion object {

View file

@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails { open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
@ -85,12 +86,12 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return JSContentPager(_pluginConfig, client, contentPager);
} }
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
} }

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -60,7 +61,7 @@ class JSComment : IPlatformComment {
if(!_hasGetReplies) if(!_hasGetReplies)
return null; return null;
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>()); val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient"); val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj); return JSCommentPager(_config!!, plugin, obj);
} }

View file

@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
abstract class JSPager<T> : IPager<T> { abstract class JSPager<T> : IPager<T> {
@ -40,7 +41,7 @@ abstract class JSPager<T> : IPager<T> {
} }
override fun hasMorePages(): Boolean { override fun hasMorePages(): Boolean {
return _hasMorePages; return _hasMorePages && !pager.isClosed;
} }
override fun nextPage() { override fun nextPage() {
@ -49,7 +50,7 @@ abstract class JSPager<T> : IPager<T> {
val pluginV8 = plugin.getUnderlyingPlugin(); val pluginV8 = plugin.getUnderlyingPlugin();
pluginV8.busy { pluginV8.busy {
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invoke("nextPage", arrayOf<Any>()); pager.invokeV8("nextPage", arrayOf<Any>());
}; };
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true; _resultChanged = true;

View file

@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
@ -57,7 +58,7 @@ class JSPlaybackTracker: IPlaybackTracker {
_client.busy { _client.busy {
if (_hasInit) { if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})"); Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeVoid("onInit", seconds); _obj.invokeV8Void("onInit", seconds);
} }
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true; _hasCalledInit = true;
@ -73,7 +74,7 @@ class JSPlaybackTracker: IPlaybackTracker {
else { else {
_client.busy { _client.busy {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); _obj.invokeV8Void("onProgress", Math.floor(seconds), isPlaying);
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_lastRequest = System.currentTimeMillis(); _lastRequest = System.currentTimeMillis();
} }
@ -86,7 +87,7 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) { synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded"); Logger.i("JSPlaybackTracker", "onConcluded");
_client.busy { _client.busy {
_obj.invokeVoid("onConcluded", -1); _obj.invokeV8Void("onConcluded", -1);
} }
} }
} }

View file

@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails { class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
@ -68,12 +69,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
return null; return null;
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return JSContentPager(_pluginConfig, client, contentPager);
} }
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
} }

View file

@ -14,6 +14,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -55,7 +57,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invoke("executeRequest", url, headers, method, body); _executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value; } as V8Value;
} }
else V8Plugin.catchScriptErrors<Any>( else V8Plugin.catchScriptErrors<Any>(
@ -63,7 +65,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invoke("executeRequest", url, headers, method, body); _executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value; } as V8Value;
try { try {
@ -110,7 +112,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invokeVoid("cleanup", null); _executor.invokeV8("cleanup", null);
}; };
} }
else V8Plugin.catchScriptErrors<Any>( else V8Plugin.catchScriptErrors<Any>(
@ -118,7 +120,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invokeVoid("cleanup", null); _executor.invokeV8("cleanup", null);
}; };
} }
} }

View file

@ -11,6 +11,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSRequestModifier: IRequestModifier { class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient; private val _plugin: JSClient;
@ -40,7 +42,7 @@ class JSRequestModifier: IRequestModifier {
return _plugin.busy { return _plugin.busy {
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers); _modifier.invokeV8("modifyRequest", url, headers);
} as V8ValueObject; } as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers); val req = JSRequest(_plugin, result, url, headers);

View file

@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getSourcePlugin import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -38,7 +39,7 @@ class JSSubtitleSource : ISubtitleSource {
throw IllegalStateException("This subtitle doesn't support getSubtitles.."); throw IllegalStateException("This subtitle doesn't support getSubtitles..");
return _obj.getSourcePlugin()?.busy { return _obj.getSourcePlugin()?.busy {
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>()); val v8String = _obj.invokeV8<V8ValueString>("getSubtitles", arrayOf<Any>());
return@busy v8String.value; return@busy v8String.value;
} ?: ""; } ?: "";
} }

View file

@ -24,6 +24,7 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails { class JSVideoDetails : JSVideo, IPlatformVideoDetails {
@ -86,7 +87,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private fun getPlaybackTrackerJS(): IPlaybackTracker? { private fun getPlaybackTrackerJS(): IPlaybackTracker? {
return _plugin.busy { return _plugin.busy {
V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") { V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>()) val tracker = _content.invokeV8<V8Value>("getPlaybackTracker", arrayOf<Any>())
?: return@catchScriptErrors null; ?: return@catchScriptErrors null;
if(tracker is V8ValueObject) if(tracker is V8ValueObject)
return@catchScriptErrors JSPlaybackTracker(_plugin, tracker); return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
@ -111,7 +112,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return _plugin.busy { return _plugin.busy {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return@busy JSContentPager(_pluginConfig, client, contentPager); return@busy JSContentPager(_pluginConfig, client, contentPager);
} }
} }
@ -130,7 +131,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? { private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
return _plugin.busy { return _plugin.busy {
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>()); val commentPager = _content.invokeV8<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return@busy null; return@busy null;

View file

@ -6,6 +6,8 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource { class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val licenseUri: String override val licenseUri: String
@ -25,7 +27,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
return null return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>()) _obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
} }
if (result !is V8ValueObject) if (result !is V8ValueObject)

View file

@ -9,6 +9,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource, class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
IDashManifestWidevineSource, JSSource { IDashManifestWidevineSource, JSSource {
@ -45,7 +47,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
return null return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>()) _obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
} }
if (result !is V8ValueObject) if (result !is V8ValueObject)

View file

@ -16,6 +16,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@ -64,7 +65,7 @@ abstract class JSSource {
return@isBusyWith null; return@isBusyWith null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
_obj.invoke("getRequestModifier", arrayOf<Any>()); _obj.invokeV8("getRequestModifier", arrayOf<Any>());
}; };
if (result !is V8ValueObject) if (result !is V8ValueObject)
@ -78,7 +79,7 @@ abstract class JSSource {
Logger.v("JSSource", "Request executor for [${type}] requesting"); Logger.v("JSSource", "Request executor for [${type}] requesting");
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf<Any>()); _obj.invokeV8("getRequestExecutor", arrayOf<Any>());
}; };
Logger.v("JSSource", "Request executor for [${type}] received"); Logger.v("JSSource", "Request executor for [${type}] received");

View file

@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource { class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
override val licenseUri: String override val licenseUri: String
@ -25,7 +26,7 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
return null return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>()) _obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
} }
if (result !is V8ValueObject) if (result !is V8ValueObject)

View file

@ -64,6 +64,7 @@ import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Collections import java.util.Collections
import java.util.UUID import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
class StateCasting { class StateCasting {
private val _scopeIO = CoroutineScope(Dispatchers.IO); private val _scopeIO = CoroutineScope(Dispatchers.IO);
@ -89,6 +90,7 @@ class StateCasting {
var _resumeCastingDevice: CastingDeviceInfo? = null; var _resumeCastingDevice: CastingDeviceInfo? = null;
private var _nsdManager: NsdManager? = null private var _nsdManager: NsdManager? = null
val isCasting: Boolean get() = activeDevice != null; val isCasting: Boolean get() = activeDevice != null;
private val _castId = AtomicInteger(0)
private val _discoveryListeners = mapOf( private val _discoveryListeners = mapOf(
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), "_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
@ -432,129 +434,112 @@ class StateCasting {
action(); action();
} }
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean { fun cancel() {
val ad = activeDevice ?: return false; _castId.incrementAndGet()
if (ad.connectionState != CastConnectionState.CONNECTED) { }
return false;
}
val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean {
return withContext(Dispatchers.IO) {
val ad = activeDevice ?: return@withContext false;
if (ad.connectionState != CastConnectionState.CONNECTED) {
return@withContext false;
}
var sourceCount = 0; val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0;
if (videoSource != null) sourceCount++; val castId = _castId.incrementAndGet()
if (audioSource != null) sourceCount++;
if (subtitleSource != null) sourceCount++;
if (sourceCount < 1) { var sourceCount = 0;
throw Exception("At least one source should be specified."); if (videoSource != null) sourceCount++;
} if (audioSource != null) sourceCount++;
if (subtitleSource != null) sourceCount++;
if (sourceCount > 1) { if (sourceCount < 1) {
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { throw Exception("At least one source should be specified.");
if (ad is AirPlayCastingDevice) { }
Logger.i(TAG, "Casting as local HLS");
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); if (sourceCount > 1) {
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as local HLS");
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as local DASH");
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
}
} else { } else {
Logger.i(TAG, "Casting as local DASH"); val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); if (isRawDash) {
} Logger.i(TAG, "Casting as raw DASH");
} else {
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
if (isRawDash) {
Logger.i(TAG, "Casting as raw DASH");
try { castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading);
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed); } else {
} catch (e: Throwable) { if (ad is FCastCastingDevice) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e); Logger.i(TAG, "Casting as DASH direct");
} castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else { } else {
if (ad is FCastCastingDevice) { Logger.i(TAG, "Casting as DASH indirect");
Logger.i(TAG, "Casting as DASH direct"); castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as DASH indirect");
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
}
} }
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
}
}
}
} else {
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
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);
} 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);
} else if(videoSource is IHLSManifestSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS");
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
} 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);
}
} else if(audioSource is IHLSManifestAudioSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS");
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
} 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);
}
} else if (videoSource is LocalVideoSource) {
Logger.i(TAG, "Casting as local video");
castLocalVideo(video, videoSource, resumePosition, speed);
} else if (audioSource is LocalAudioSource) {
Logger.i(TAG, "Casting as local audio");
castLocalAudio(video, audioSource, resumePosition, speed);
} else if (videoSource is JSDashManifestRawSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
}
}
} else if (audioSource is JSDashManifestRawAudioSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
} }
} }
} else { } else {
var str = listOf( val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, val url = getLocalUrl(ad);
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, val id = UUID.randomUUID();
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
).filterNotNull().joinToString(", ");
throw UnsupportedCastException(str);
}
}
return true; 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);
} 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);
} else if(videoSource is IHLSManifestSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS");
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
} 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);
}
} else if(audioSource is IHLSManifestAudioSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS");
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
} 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);
}
} else if (videoSource is LocalVideoSource) {
Logger.i(TAG, "Casting as local video");
castLocalVideo(video, videoSource, resumePosition, speed);
} else if (audioSource is LocalAudioSource) {
Logger.i(TAG, "Casting as local audio");
castLocalAudio(video, audioSource, resumePosition, speed);
} else if (videoSource is JSDashManifestRawSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
} else if (audioSource is JSDashManifestRawAudioSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
} else {
var str = listOf(
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
).filterNotNull().joinToString(", ");
throw UnsupportedCastException(str);
}
}
return@withContext true;
}
} }
fun resumeVideo(): Boolean { fun resumeVideo(): Boolean {
@ -1236,7 +1221,7 @@ class StateCasting {
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> { 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<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
cleanExecutors() cleanExecutors()
@ -1283,20 +1268,48 @@ class StateCasting {
} }
} }
var dashContent = withContext(Dispatchers.IO) { var dashContent: String = withContext(Dispatchers.IO) {
stopVideo()
//TODO: Include subtitlesURl in the future //TODO: Include subtitlesURl in the future
return@withContext if (audioSource != null && videoSource != null) { val deferred = if (audioSource != null && videoSource != null) {
JSDashManifestMergingRawSource(videoSource, audioSource).generate() JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO)
} else if (audioSource != null) { } else if (audioSource != null) {
audioSource.generate() audioSource.generateAsync(_scopeIO)
} else if (videoSource != null) { } else if (videoSource != null) {
videoSource.generate() videoSource.generateAsync(_scopeIO)
} else { } else {
Logger.e(TAG, "Expected at least audio or video to be set") Logger.e(TAG, "Expected at least audio or video to be set")
null null
} }
if (deferred != null) {
try {
withContext(Dispatchers.Main) {
if (deferred.estDuration >= 0) {
onLoadingEstimate?.invoke(deferred.estDuration)
} else {
onLoading?.invoke(true)
}
}
deferred.await()
} finally {
if (castId == _castId.get()) {
withContext(Dispatchers.Main) {
onLoading?.invoke(false)
}
}
}
} else {
return@withContext null
}
} ?: throw Exception("Dash is null") } ?: throw Exception("Dash is null")
if (castId != _castId.get()) {
Log.i(TAG, "Get DASH cancelled.")
return emptyList()
}
for (representation in representationRegex.findAll(dashContent)) { for (representation in representationRegex.findAll(dashContent)) {
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found") val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
dashContent = mediaInitializationRegex.replace(dashContent) { dashContent = mediaInitializationRegex.replace(dashContent) {

View file

@ -17,6 +17,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.IV8Convertable import com.futo.platformplayer.engine.internal.IV8Convertable
import com.futo.platformplayer.engine.internal.V8BindObject import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
@ -668,7 +669,7 @@ class PackageHttp: V8Package {
if(hasOpen && _listeners?.isClosed != true) { if(hasOpen && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
_listeners?.invokeVoid("open", arrayOf<Any>()); _listeners?.invokeV8Void("open", arrayOf<Any>());
} }
} }
catch(ex: Throwable){ catch(ex: Throwable){
@ -680,7 +681,7 @@ class PackageHttp: V8Package {
if(hasMessage && _listeners?.isClosed != true) { if(hasMessage && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
_listeners?.invokeVoid("message", msg); _listeners?.invokeV8Void("message", msg);
} }
} }
catch(ex: Throwable) {} catch(ex: Throwable) {}
@ -691,7 +692,7 @@ class PackageHttp: V8Package {
{ {
try { try {
_package._plugin.busy { _package._plugin.busy {
_listeners?.invokeVoid("closing", code, reason); _listeners?.invokeV8Void("closing", code, reason);
} }
} }
catch(ex: Throwable){ catch(ex: Throwable){
@ -704,7 +705,7 @@ class PackageHttp: V8Package {
if(hasClosed && _listeners?.isClosed != true) { if(hasClosed && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
_listeners?.invokeVoid("closed", code, reason); _listeners?.invokeV8Void("closed", code, reason);
} }
} }
catch(ex: Throwable){ catch(ex: Throwable){
@ -722,7 +723,7 @@ class PackageHttp: V8Package {
if(hasFailure && _listeners?.isClosed != true) { if(hasFailure && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy { _package._plugin.busy {
_listeners?.invokeVoid("failure", exception.message); _listeners?.invokeV8Void("failure", exception.message);
} }
} }
catch(ex: Throwable){ catch(ex: Throwable){

View file

@ -4,6 +4,7 @@ import android.app.PictureInPictureParams
import android.app.RemoteAction import android.app.RemoteAction
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
@ -79,7 +80,9 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
@ -806,6 +809,8 @@ class VideoDetailView : ConstraintLayout {
_lastVideoSource = null; _lastVideoSource = null;
_lastAudioSource = null; _lastAudioSource = null;
_lastSubtitleSource = null; _lastSubtitleSource = null;
_cast.cancel()
StateCasting.instance.cancel()
video = null; video = null;
_container_content_liveChat?.close(); _container_content_liveChat?.close();
_player.clear(); _player.clear();
@ -1898,11 +1903,46 @@ class VideoDetailView : ConstraintLayout {
} }
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)") Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)
}
if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)) { private fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
_cast.setVideoDetails(video, resumePositionMs / 1000); fragment.lifecycleScope.launch(Dispatchers.IO) {
setCastEnabled(true); try {
} else throw IllegalStateException("Disconnected cast during loading"); val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
else null
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
try {
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
_cast.setLoading(it)
}, onLoadingEstimate = {
_cast.setLoading(it)
})
if (castingSucceeded) {
withContext(Dispatchers.Main) {
_cast.setVideoDetails(video, resumePositionMs / 1000);
setCastEnabled(true);
}
}
} catch (e: ScriptReloadRequiredException) {
Log.i(TAG, "Reload required exception", e)
if (plugin == null)
throw e
if (startId != -1 && plugin.getUnderlyingPlugin().runtimeId != startId)
throw e
StatePlatform.instance.handleReloadRequired(e, {
fetchVideo()
});
}
} catch (e: Throwable) {
Logger.e(TAG, "loadCurrentVideoCast", e)
}
}
} }
//Events //Events
@ -2415,7 +2455,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true)) else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
@ -2430,7 +2470,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
@ -2446,7 +2486,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else else
_player.swapSubtitles(fragment.lifecycleScope, toSet); _player.swapSubtitles(fragment.lifecycleScope, toSet);
@ -2553,8 +2593,7 @@ class VideoDetailView : ConstraintLayout {
_cast.visibility = View.VISIBLE; _cast.visibility = View.VISIBLE;
} else { } else {
StateCasting.instance.stopVideo(); StateCasting.instance.stopVideo();
_cast.stopTimeJob(); _cast.cancel()
_cast.visibility = View.GONE;
if (video?.isLive == false) { if (video?.isLive == false) {
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed()); _player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());

View file

@ -395,8 +395,9 @@ class StatePlatform {
} }
suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) { suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var removed: MutableList<IPlatformClient>;
synchronized(_clientsLock) { synchronized(_clientsLock) {
val removed = _enabledClients.toMutableList(); removed = _enabledClients.toMutableList();
_enabledClients.clear(); _enabledClients.clear();
for (id in ids) { for (id in ids) {
val client = getClient(id); val client = getClient(id);
@ -412,11 +413,11 @@ class StatePlatform {
} }
_enabledClientsPersistent.set(*ids); _enabledClientsPersistent.set(*ids);
_enabledClientsPersistent.save(); _enabledClientsPersistent.save();
}
for (oldClient in removed) { for (oldClient in removed) {
oldClient.disable(); oldClient.disable();
onSourceDisabled.emit(oldClient); onSourceDisabled.emit(oldClient);
}
} }
afterLoad?.invoke(); afterLoad?.invoke();
}; };

View file

@ -0,0 +1,381 @@
package com.futo.platformplayer.views
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.OvershootInterpolator
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.toColorInt
import kotlin.math.*
import kotlin.random.Random
import com.futo.platformplayer.UIDialogs
class TargetTapLoaderView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val primaryColor = "#2D63ED".toColorInt()
private val inactiveGlobalAlpha = 110
private val idleSpeedMultiplier = .015f
private val overshootInterpolator = OvershootInterpolator(1.5f)
private val floatAccel = .03f
private val idleMaxSpeed = .35f
private val idleInitialTargets = 10
private val idleHintText = "Waiting for media to become available"
private var expectedDurationMs: Long? = null
private var loadStartTime = 0L
private var playStartTime = 0L
private var loaderFinished = false
private var forceIndeterminate= false
private var lastFrameTime = System.currentTimeMillis()
private var score = 0
private var isPlaying = false
private val targets = mutableListOf<Target>()
private val particles = mutableListOf<Particle>()
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(0.7f, 1f, 1f, 1f)
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics)
textAlign = Paint.Align.LEFT
setShadowLayer(4f, 0f, 0f, Color.BLACK)
typeface = Typeface.DEFAULT_BOLD
}
private val idleHintPaint = Paint(textPaint).apply {
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics)
typeface = Typeface.DEFAULT
setShadowLayer(2f, 0f, 0f, Color.BLACK)
}
private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = primaryColor }
private val spinnerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = primaryColor; strokeWidth = 12f
style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND
}
private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.argb(50, 0, 0, 0) }
private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val backgroundPaint = Paint()
private var spinnerShader: SweepGradient? = null
private var spinnerAngle = 0f
private val MIN_SPAWN_RATE = 1f
private val MAX_SPAWN_RATE = 20.0f
private val HIT_RATE_INCREMENT = 0.15f
private val MISS_RATE_DECREMENT = 0.09f
private var spawnRate = MIN_SPAWN_RATE
private val frameRunnable = object : Runnable {
override fun run() { invalidate(); if (!loaderFinished) postDelayed(this, 16L) }
}
init { setOnTouchListener { _, e -> if (e.action == MotionEvent.ACTION_DOWN) handleTap(e.x, e.y); true } }
fun startLoader(durationMs: Long? = null) {
val alreadyRunning = !loaderFinished
if (alreadyRunning && durationMs == null) {
expectedDurationMs = null
forceIndeterminate = true
return
}
expectedDurationMs = durationMs?.takeIf { it > 0 }
forceIndeterminate = expectedDurationMs == null
loaderFinished = false
isPlaying = false
score = 0
particles.clear()
spawnRate = MIN_SPAWN_RATE
post { if (targets.isEmpty()) prepopulateIdleTargets() }
loadStartTime = System.currentTimeMillis()
playStartTime = 0
removeCallbacks(frameRunnable)
post(frameRunnable)
if (!isIndeterminate) {
postDelayed({
if (!loaderFinished) {
forceIndeterminate = true
expectedDurationMs = null
}
}, expectedDurationMs!!)
}
}
fun finishLoader() {
loaderFinished = true
particles.clear()
isPlaying = false
invalidate()
}
fun stopAndResetLoader() {
if (score > 0) {
val elapsed = (System.currentTimeMillis() - (if (playStartTime > 0) playStartTime else loadStartTime)) / 1000.0
UIDialogs.toast("Nice! score $score | ${"%.1f".format(score / elapsed)} / s")
score = 0
}
loaderFinished = true
isPlaying = false
targets.clear()
particles.clear()
removeCallbacks(frameRunnable)
invalidate()
}
private val isIndeterminate get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L
private fun handleTap(x: Float, y: Float) {
val idx = targets.indexOfFirst { !it.hit && hypot(x - it.x, y - it.y) <= it.radius }
if (idx >= 0) {
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
val t = targets[idx]
t.hit = true; t.hitTime = System.currentTimeMillis()
accelerateSpawnRate()
score += if (!isIndeterminate) 10 else 5
spawnParticles(t.x, t.y, t.radius)
if (!isPlaying) {
isPlaying = true
playStartTime = System.currentTimeMillis()
score = 0
spawnRate = MIN_SPAWN_RATE
targets.retainAll { it === t }
spawnTarget()
}
} else if (isPlaying) decelerateSpawnRate()
}
private inline fun accelerateSpawnRate() {
spawnRate = (spawnRate + HIT_RATE_INCREMENT).coerceAtMost(MAX_SPAWN_RATE)
}
private inline fun decelerateSpawnRate() {
spawnRate = (spawnRate - MISS_RATE_DECREMENT).coerceAtLeast(MIN_SPAWN_RATE)
}
private fun spawnTarget() {
if (loaderFinished || width == 0 || height == 0) {
postDelayed({ spawnTarget() }, 200L); return
}
if (!isPlaying) {
postDelayed({ spawnTarget() }, 500L); return
}
val radius = Random.nextInt(40, 80).toFloat()
val x = Random.nextFloat() * (width - 2 * radius) + radius
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
val baseSpeed = Random.nextFloat() + .1f
val speed = baseSpeed
val angle = Random.nextFloat() * TAU
val vx = cos(angle) * speed
val vy = sin(angle) * speed
val alpha = Random.nextInt(150, 255)
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
val delay = (1000f / spawnRate).roundToLong()
postDelayed({ spawnTarget() }, delay)
}
private fun prepopulateIdleTargets() {
if (width == 0 || height == 0) {
post { prepopulateIdleTargets() }
return
}
repeat(idleInitialTargets) {
val radius = Random.nextInt(40, 80).toFloat()
val x = Random.nextFloat() * (width - 2 * radius) + radius
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
val angle = Random.nextFloat() * TAU
val speed = (Random.nextFloat() * .3f + .05f) * idleSpeedMultiplier
val vx = cos(angle) * speed
val vy = sin(angle) * speed
val alpha = Random.nextInt(60, 110)
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
}
}
private fun spawnParticles(cx: Float, cy: Float, radius: Float) {
repeat(12) {
val angle = Random.nextFloat() * TAU
val speed = Random.nextFloat() * 5f + 2f
val vx = cos(angle) * speed
val vy = sin(angle) * speed
val col = ColorUtils.setAlphaComponent(primaryColor, Random.nextInt(120, 255))
particles += Particle(cx, cy, vx, vy, System.currentTimeMillis(), col)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val now = System.currentTimeMillis()
val deltaMs = now - lastFrameTime
lastFrameTime = now
drawBackground(canvas)
drawTargets(canvas, now)
drawParticles(canvas, now)
if (!loaderFinished) {
if (isIndeterminate) drawIndeterminateSpinner(canvas, deltaMs)
else drawDeterministicProgressBar(canvas, now)
}
if (isPlaying) {
val margin = 24f
val scoreTxt = "Score: $score"
val speedTxt = "Speed: ${"%.2f".format(spawnRate)}/s"
val maxWidth = width - margin
val needRight = max(textPaint.measureText(scoreTxt), textPaint.measureText(speedTxt)) > maxWidth
val alignX = if (needRight) (width - margin) else margin
textPaint.textAlign = if (needRight) Paint.Align.RIGHT else Paint.Align.LEFT
canvas.drawText(scoreTxt, alignX, textPaint.textSize + margin, textPaint)
canvas.drawText(speedTxt, alignX, 2*textPaint.textSize + margin + 4f, textPaint)
textPaint.textAlign = Paint.Align.LEFT
}
else if (loaderFinished)
canvas.drawText("Loading Complete!", width/2f, height/2f, textPaint.apply { textAlign = Paint.Align.CENTER })
else {
idleHintPaint.textAlign = Paint.Align.CENTER
canvas.drawText(idleHintText, width / 2f, height - 48f, idleHintPaint)
}
}
private fun drawBackground(canvas: Canvas) {
val colors = intArrayOf(
Color.rgb(20, 20, 40),
Color.rgb(15, 15, 30),
Color.rgb(10, 10, 20),
Color.rgb( 5, 5, 10),
Color.BLACK
)
val pos = floatArrayOf(0f, 0.25f, 0.5f, 0.75f, 1f)
if (backgroundPaint.shader == null) {
backgroundPaint.shader = LinearGradient(
0f, 0f, 0f, height.toFloat(),
colors, pos,
Shader.TileMode.CLAMP
)
}
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint)
}
private fun drawTargets(canvas: Canvas, now: Long) {
val expireMsActive = if (isIndeterminate) 2500L else 1500L
val it = targets.iterator()
while (it.hasNext()) {
val t = it.next()
if (t.hit && now - t.hitTime > 300L) { it.remove(); continue }
if (isPlaying && !t.hit && now - t.spawnTime > expireMsActive) {
it.remove(); decelerateSpawnRate(); continue
}
t.x += t.vx; t.y += t.vy
t.vx += (Random.nextFloat() - .5f) * floatAccel
t.vy += (Random.nextFloat() - .5f) * floatAccel
val speedCap = if (isPlaying) Float.MAX_VALUE else idleMaxSpeed
val mag = hypot(t.vx, t.vy)
if (mag > speedCap) {
val s = speedCap / mag
t.vx *= s; t.vy *= s
}
if (t.x - t.radius < 0 || t.x + t.radius > width) t.vx *= -1
if (t.y - t.radius < 0 || t.y + t.radius > height) t.vy *= -1
val scale = if (t.hit) 1f - ((now - t.hitTime) / 300f).coerceIn(0f,1f)
else {
val e = now - t.spawnAnimStart
if (e < 300L) overshootInterpolator.getInterpolation(e/300f)
else 1f + .02f * sin(((now - t.spawnAnimStart)/1000f)*TAU + t.pulseOffset)
}
val animAlpha = if (t.hit) ((1f - scale)*255).toInt() else 255
val globalAlpha = if (isPlaying) 255 else inactiveGlobalAlpha
val alpha = (animAlpha * t.baseAlpha /255f * globalAlpha/255f).toInt().coerceAtMost(255)
val r = max(1f, t.radius*scale)
val outerCol = ColorUtils.setAlphaComponent(primaryColor, alpha)
val midCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.7f).toInt())
val innerCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.4f).toInt())
outerRingPaint.color = outerCol; middleRingPaint.color = midCol; centerDotPaint.color = innerCol
glowPaint.shader = RadialGradient(t.x, t.y, r, outerCol, Color.TRANSPARENT, Shader.TileMode.CLAMP)
canvas.drawCircle(t.x, t.y, r*1.2f, glowPaint)
canvas.drawCircle(t.x+4f, t.y+4f, r, shadowPaint)
canvas.drawCircle(t.x, t.y, r, outerRingPaint)
canvas.drawCircle(t.x, t.y, r*.66f, middleRingPaint)
canvas.drawCircle(t.x, t.y, r*.33f, centerDotPaint)
}
}
private fun drawParticles(canvas: Canvas, now: Long) {
val lifespan = 400L
val it = particles.iterator()
while (it.hasNext()) {
val p = it.next()
val age = now - p.startTime
if (age > lifespan) { it.remove(); continue }
val a = ((1f - age/lifespan.toFloat())*255).toInt()
particlePaint.color = ColorUtils.setAlphaComponent(p.baseColor, a)
p.x += p.vx; p.y += p.vy
canvas.drawCircle(p.x, p.y, 6f, particlePaint)
}
}
private fun drawDeterministicProgressBar(canvas: Canvas, now: Long) {
val dur = expectedDurationMs ?: return
val prog = ((now - loadStartTime) / dur.toFloat()).coerceIn(0f, 1f)
val eased = AccelerateDecelerateInterpolator().getInterpolation(prog)
val h = 20f; val r=10f
canvas.drawRoundRect(RectF(0f, height-h, width*eased, height.toFloat()), r, r, progressBarPaint)
}
private fun drawIndeterminateSpinner(canvas: Canvas, dt: Long) {
val cx=width/2f; val cy=height/2f; val r=min(width,height)/6f
spinnerAngle = (spinnerAngle + .25f*dt)%360f
if(spinnerShader == null) spinnerShader = SweepGradient(cx,cy,intArrayOf(Color.TRANSPARENT,Color.WHITE,Color.TRANSPARENT),floatArrayOf(0f,.5f,1f))
spinnerPaint.shader = spinnerShader
val glow = Paint(spinnerPaint).apply{ maskFilter = BlurMaskFilter(15f,BlurMaskFilter.Blur.SOLID) }
val sweep = 270f
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,glow)
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,spinnerPaint)
}
private data class Target(
var x: Float,
var y: Float,
val radius: Float,
val spawnTime: Long,
var hit: Boolean = false,
var hitTime: Long = 0L,
val baseAlpha: Int = 255,
var vx: Float=0f,
var vy:Float=0f,
val spawnAnimStart: Long = System.currentTimeMillis(),
val pulseOffset: Float = Random.nextFloat() * TAU
)
private data class Particle(
var x:Float,
var y:Float,
val vx:Float,
val vy:Float,
val startTime:Long,
val baseColor:Int
)
private companion object { private const val TAU = (2 * Math.PI).toFloat() }
}

View file

@ -30,6 +30,7 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.formatDuration import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -54,6 +55,7 @@ class CastView : ConstraintLayout {
private val _timeBar: DefaultTimeBar; private val _timeBar: DefaultTimeBar;
private val _background: FrameLayout; private val _background: FrameLayout;
private val _gestureControlView: GestureControlView; private val _gestureControlView: GestureControlView;
private val _loaderGame: TargetTapLoaderView
private var _scope: CoroutineScope = CoroutineScope(Dispatchers.Main); private var _scope: CoroutineScope = CoroutineScope(Dispatchers.Main);
private var _updateTimeJob: Job? = null; private var _updateTimeJob: Job? = null;
private var _inPictureInPicture: Boolean = false; private var _inPictureInPicture: Boolean = false;
@ -88,6 +90,9 @@ class CastView : ConstraintLayout {
_timeBar = findViewById(R.id.time_progress); _timeBar = findViewById(R.id.time_progress);
_background = findViewById(R.id.layout_background); _background = findViewById(R.id.layout_background);
_gestureControlView = findViewById(R.id.gesture_control); _gestureControlView = findViewById(R.id.gesture_control);
_loaderGame = findViewById(R.id.loader_overlay)
_loaderGame.visibility = View.GONE
_gestureControlView.fullScreenGestureEnabled = false _gestureControlView.fullScreenGestureEnabled = false
_gestureControlView.setupTouchArea(); _gestureControlView.setupTouchArea();
_gestureControlView.onSpeedHoldStart.subscribe { _gestureControlView.onSpeedHoldStart.subscribe {
@ -197,6 +202,12 @@ class CastView : ConstraintLayout {
_updateTimeJob = null; _updateTimeJob = null;
} }
fun cancel() {
stopTimeJob()
setLoading(false)
visibility = View.GONE
}
fun stopAllGestures() { fun stopAllGestures() {
_gestureControlView.stopAllGestures(); _gestureControlView.stopAllGestures();
} }
@ -279,6 +290,7 @@ class CastView : ConstraintLayout {
_textDuration.text = (video.duration * 1000).formatDuration(); _textDuration.text = (video.duration * 1000).formatDuration();
_timeBar.setPosition(position); _timeBar.setPosition(position);
_timeBar.setDuration(video.duration); _timeBar.setDuration(video.duration);
setLoading(false)
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@ -295,6 +307,7 @@ class CastView : ConstraintLayout {
_updateTimeJob?.cancel(); _updateTimeJob?.cancel();
_updateTimeJob = null; _updateTimeJob = null;
_scope.cancel(); _scope.cancel();
setLoading(false)
} }
private fun getPlaybackStateCompat(): Int { private fun getPlaybackStateCompat(): Int {
@ -305,4 +318,19 @@ class CastView : ConstraintLayout {
else -> PlaybackStateCompat.STATE_PAUSED; else -> PlaybackStateCompat.STATE_PAUSED;
} }
} }
fun setLoading(isLoading: Boolean) {
if (isLoading) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader()
} else {
_loaderGame.visibility = View.GONE
_loaderGame.stopAndResetLoader()
}
}
fun setLoading(expectedDurationMs: Int) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong())
}
} }

View file

@ -64,6 +64,7 @@ class LiveChatOverlay : LinearLayout {
private val _overlayRaid: ConstraintLayout; private val _overlayRaid: ConstraintLayout;
private val _overlayRaid_Name: TextView; private val _overlayRaid_Name: TextView;
private val _overlayRaid_Message: TextView;
private val _overlayRaid_Thumbnail: ImageView; private val _overlayRaid_Thumbnail: ImageView;
private val _overlayRaid_ButtonGo: Button; private val _overlayRaid_ButtonGo: Button;
@ -146,6 +147,7 @@ class LiveChatOverlay : LinearLayout {
_overlayRaid = findViewById(R.id.overlay_raid); _overlayRaid = findViewById(R.id.overlay_raid);
_overlayRaid_Name = findViewById(R.id.raid_name); _overlayRaid_Name = findViewById(R.id.raid_name);
_overlayRaid_Message = findViewById(R.id.textRaidMessage);
_overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail); _overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail);
_overlayRaid_ButtonGo = findViewById(R.id.raid_button_go); _overlayRaid_ButtonGo = findViewById(R.id.raid_button_go);
_overlayRaid_ButtonDismiss = findViewById(R.id.raid_button_prevent); _overlayRaid_ButtonDismiss = findViewById(R.id.raid_button_prevent);
@ -371,7 +373,14 @@ class LiveChatOverlay : LinearLayout {
else else
_overlayRaid.visibility = View.GONE; _overlayRaid.visibility = View.GONE;
_overlayRaid_ButtonGo.visibility = if (raid?.isOutgoing == true) View.VISIBLE else View.GONE if(raid?.isOutgoing ?: false) {
_overlayRaid_ButtonGo.visibility = View.VISIBLE
_overlayRaid_Message.text = "Viewers are raiding";
}
else {
_overlayRaid_ButtonGo.visibility = View.GONE;
_overlayRaid_Message.text = "Raid incoming from";
}
} }
} }
fun setViewCount(viewCount: Int) { fun setViewCount(viewCount: Int) {

View file

@ -44,6 +44,7 @@ import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -154,10 +155,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onChapterClicked = Event1<IChapter>(); val onChapterClicked = Event1<IChapter>();
private val loaderOverlay: FrameLayout private val _loaderGame: TargetTapLoaderView
private val loaderIndeterminate: android.widget.ProgressBar
private val loaderDeterminate: android.widget.ProgressBar
private var determinateAnimator: ValueAnimator? = null
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
@ -199,13 +197,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration); _control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration);
_control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause); _control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause);
loaderOverlay = findViewById(R.id.loader_overlay) _loaderGame = findViewById(R.id.loader_overlay)
loaderIndeterminate = findViewById(R.id.loader_indeterminate) _loaderGame.visibility = View.GONE
loaderDeterminate = findViewById(R.id.loader_determinate)
loaderOverlay.visibility = View.GONE
loaderIndeterminate.visibility = View.GONE
loaderDeterminate.visibility = View.GONE
_control_chapter.setOnClickListener { _control_chapter.setOnClickListener {
_currentChapter?.let { _currentChapter?.let {
@ -884,33 +877,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
override fun setLoading(isLoading: Boolean) { override fun setLoading(isLoading: Boolean) {
determinateAnimator?.cancel()
if (isLoading) { if (isLoading) {
loaderOverlay.visibility = View.VISIBLE _loaderGame.visibility = View.VISIBLE
loaderIndeterminate.visibility = View.VISIBLE _loaderGame.startLoader()
loaderDeterminate.visibility = View.GONE
} else { } else {
loaderOverlay.visibility = View.GONE _loaderGame.visibility = View.GONE
loaderIndeterminate.visibility = View.GONE _loaderGame.stopAndResetLoader()
loaderDeterminate.visibility = View.GONE
} }
} }
override fun setLoading(expectedDurationMs: Int) { override fun setLoading(expectedDurationMs: Int) {
determinateAnimator?.cancel() _loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong())
loaderOverlay.visibility = View.VISIBLE
loaderIndeterminate.visibility = View.GONE
loaderDeterminate.visibility = View.VISIBLE
loaderDeterminate.max = expectedDurationMs
loaderDeterminate.progress = 0
determinateAnimator = ValueAnimator.ofInt(0, expectedDurationMs).apply {
duration = expectedDurationMs.toLong()
addUpdateListener { anim ->
loaderDeterminate.progress = anim.animatedValue as Int
}
start()
}
} }
} }

View file

@ -3,11 +3,13 @@ package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
@ -30,6 +32,8 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.source.SingleSampleMediaSource import androidx.media3.exoplayer.source.SingleSampleMediaSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -53,9 +57,14 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
@ -72,6 +81,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.abs import kotlin.math.abs
abstract class FutoVideoPlayerBase : RelativeLayout { abstract class FutoVideoPlayerBase : RelativeLayout {
@ -116,7 +126,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private var _didCallSourceChange = false; private var _didCallSourceChange = false;
private var _lastState: Int = -1; private var _lastState: Int = -1;
private val _swapIdAudio = AtomicInteger(0)
private val _swapIdVideo = AtomicInteger(0)
var targetTrackVideoHeight = -1 var targetTrackVideoHeight = -1
private set private set
@ -435,13 +446,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean { private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean {
setLoading(false)
val swapId = _swapIdVideo.incrementAndGet()
_lastGeneratedDash = null; _lastGeneratedDash = null;
val didSet = when(videoSource) { val didSet = when(videoSource) {
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; } is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; } is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true } is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true }
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;} is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume); is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume, swapId);
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; } is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; } is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; } is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
@ -452,11 +465,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return didSet; return didSet;
} }
private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean { private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean {
setLoading(false)
val swapId = _swapIdAudio.incrementAndGet()
val didSet = when(audioSource) { val didSet = when(audioSource) {
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; } is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; } is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; }
is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; } is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; }
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume); is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume, swapId);
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; } is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; } is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
null -> { _lastAudioMediaSource = null; true; } null -> { _lastAudioMediaSource = null; true; }
@ -563,7 +578,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}.createMediaSource(MediaItem.fromUri(videoSource.url)) }.createMediaSource(MediaItem.fromUri(videoSource.url))
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean { private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean, swapId: Int): Boolean {
Logger.i(TAG, "Loading VideoSource [Dash]"); Logger.i(TAG, "Loading VideoSource [Dash]");
if(videoSource.hasGenerate) { if(videoSource.hasGenerate) {
@ -582,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
} }
val generated = generatedDef.await(); val generated = generatedDef.await();
if (_swapIdVideo.get() != swapId) {
return@launch
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
setLoading(false) setLoading(false)
} }
@ -707,7 +726,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean { private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean, swapId: Int): Boolean {
Logger.i(TAG, "Loading AudioSource [DashRaw]"); Logger.i(TAG, "Loading AudioSource [DashRaw]");
if(audioSource.hasGenerate) { if(audioSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
@ -725,6 +744,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
} }
val generated = generatedDef.await(); val generated = generatedDef.await();
if (_swapIdAudio.get() != swapId) {
return@launch
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
setLoading(false) setLoading(false)
} }
@ -888,6 +910,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
fun clear() { fun clear() {
exoPlayer?.player?.stop(); exoPlayer?.player?.stop();
exoPlayer?.player?.clearMediaItems(); exoPlayer?.player?.clearMediaItems();
setLoading(false)
_swapIdVideo.incrementAndGet()
_swapIdAudio.incrementAndGet()
_lastVideoMediaSource = null; _lastVideoMediaSource = null;
_lastAudioMediaSource = null; _lastAudioMediaSource = null;
_lastSubtitleMediaSource = null; _lastSubtitleMediaSource = null;

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#ff5a5d5a"
android:centerY="0.75"
android:endColor="#ff5a5d5a"
android:startColor="#ff5a5d5a" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#80ffb600"
android:centerY="0.75"
android:endColor="#80ffd300"
android:startColor="#80ffd300" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:endColor="#3333FF"
android:startColor="#3333FF" />
</shape>
</clip>
</item>
</layer-list>

View file

@ -5,9 +5,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/black"> android:background="@color/black">
<com.futo.platformplayer.views.others.CircularProgressBar <com.futo.platformplayer.views.TargetTapLoaderView
android:id="@+id/test_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="240dp" />
app:progress="0%"
app:strokeWidth="20dp" />
</FrameLayout> </FrameLayout>

View file

@ -65,35 +65,10 @@
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
<FrameLayout <com.futo.platformplayer.views.TargetTapLoaderView
android:id="@+id/loader_overlay" android:id="@+id/loader_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginBottom="6dp" android:visibility="gone" />
android:background="@color/black"
android:clickable="true"
android:focusable="true"
android:visibility="gone">
<ProgressBar
android:id="@+id/loader_indeterminate"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
<ProgressBar
android:id="@+id/loader_determinate"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="false"
android:max="100"
android:progress="0"
android:visibility="gone"/>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -189,4 +189,14 @@
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" /> app:layout_constraintRight_toRightOf="parent" />
<com.futo.platformplayer.views.TargetTapLoaderView
android:id="@+id/loader_overlay"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -1 +1 @@
Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 Subproject commit 736c6b953a4613145e32010ff5ee5b08be1baac6

@ -1 +1 @@
Subproject commit b811f8bdfbbff73cf0d7581c9d7596911cb132b6 Subproject commit 6880b30b71800f6d22ddcb692f3c1c09e745315b

@ -1 +1 @@
Subproject commit 214ac1dfcc985f533d9db7d128a8315bc55fa854 Subproject commit 8c0f03f5fbc9b4e499437b85c757ec40cb7c0126

@ -1 +1 @@
Subproject commit 08346f917753694e14bc1caa784aa87066a2ab84 Subproject commit 8de3ab18f5a154f49f02e2bee1b126a302df260d

@ -1 +1 @@
Subproject commit 48d98c1f0cd80e9e569280423ae404e56047c883 Subproject commit 2b724f21a727c3fefe16adb38f06aa8730b1b8ec

@ -1 +1 @@
Subproject commit d11543001150f96f3383d83fec3341d9321746b8 Subproject commit 850eb8122dd8348904d55ceb9c3a26b49bcb8a45

@ -1 +1 @@
Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 Subproject commit 736c6b953a4613145e32010ff5ee5b08be1baac6

@ -1 +1 @@
Subproject commit b811f8bdfbbff73cf0d7581c9d7596911cb132b6 Subproject commit 6880b30b71800f6d22ddcb692f3c1c09e745315b

@ -1 +1 @@
Subproject commit 214ac1dfcc985f533d9db7d128a8315bc55fa854 Subproject commit 8c0f03f5fbc9b4e499437b85c757ec40cb7c0126

@ -1 +1 @@
Subproject commit 08346f917753694e14bc1caa784aa87066a2ab84 Subproject commit 8de3ab18f5a154f49f02e2bee1b126a302df260d

@ -1 +1 @@
Subproject commit 48d98c1f0cd80e9e569280423ae404e56047c883 Subproject commit 2b724f21a727c3fefe16adb38f06aa8730b1b8ec

@ -1 +1 @@
Subproject commit f87f00ab9e1262e300246b8963591bdf3a8fada7 Subproject commit 278e3c2febf853a71f0719f9b0ea98339a0214ac