mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-13 11:39:49 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into shorts-tab
This commit is contained in:
commit
3310ac6008
41 changed files with 791 additions and 246 deletions
|
@ -238,6 +238,9 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
|||
else
|
||||
V8Deferred<T>(underlyingDef);
|
||||
|
||||
if(def.estDuration > 0)
|
||||
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
|
||||
|
||||
val promise = this;
|
||||
plugin.busy {
|
||||
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);
|
||||
if(result is V8ValuePromise) {
|
||||
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
|
||||
}
|
||||
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);
|
||||
if(result is V8ValuePromise) {
|
||||
return result.toV8ValueAsync(this.getSourcePlugin()!!);
|
||||
}
|
||||
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));
|
||||
}
|
|
@ -32,7 +32,6 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.whenStateAtLeast
|
||||
import androidx.lifecycle.withStateAtLeast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
|
@ -115,7 +114,6 @@ import java.io.PrintWriter
|
|||
import java.io.StringWriter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
@ -613,6 +611,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
)
|
||||
}
|
||||
|
||||
//startActivity(Intent(this, TestActivity::class.java))
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -2,12 +2,24 @@ package com.futo.platformplayer.activities
|
|||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TestActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_test);
|
||||
|
||||
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
|
||||
view.startLoader(10000)
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(5000)
|
||||
view.startLoader()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||
|
@ -85,12 +86,12 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
|
@ -60,7 +61,7 @@ class JSComment : IPlatformComment {
|
|||
if(!_hasGetReplies)
|
||||
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");
|
||||
return JSCommentPager(_config!!, plugin, obj);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
|
||||
abstract class JSPager<T> : IPager<T> {
|
||||
|
@ -40,7 +41,7 @@ abstract class JSPager<T> : IPager<T> {
|
|||
}
|
||||
|
||||
override fun hasMorePages(): Boolean {
|
||||
return _hasMorePages;
|
||||
return _hasMorePages && !pager.isClosed;
|
||||
}
|
||||
|
||||
override fun nextPage() {
|
||||
|
@ -49,7 +50,7 @@ abstract class JSPager<T> : IPager<T> {
|
|||
val pluginV8 = plugin.getUnderlyingPlugin();
|
||||
pluginV8.busy {
|
||||
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;
|
||||
_resultChanged = true;
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
|
@ -57,7 +58,7 @@ class JSPlaybackTracker: IPlaybackTracker {
|
|||
_client.busy {
|
||||
if (_hasInit) {
|
||||
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
|
||||
_obj.invokeVoid("onInit", seconds);
|
||||
_obj.invokeV8Void("onInit", seconds);
|
||||
}
|
||||
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
|
||||
_hasCalledInit = true;
|
||||
|
@ -73,7 +74,7 @@ class JSPlaybackTracker: IPlaybackTracker {
|
|||
else {
|
||||
_client.busy {
|
||||
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));
|
||||
_lastRequest = System.currentTimeMillis();
|
||||
}
|
||||
|
@ -86,7 +87,7 @@ class JSPlaybackTracker: IPlaybackTracker {
|
|||
synchronized(_obj) {
|
||||
Logger.i("JSPlaybackTracker", "onConcluded");
|
||||
_client.busy {
|
||||
_obj.invokeVoid("onConcluded", -1);
|
||||
_obj.invokeV8Void("onConcluded", -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
|
@ -68,12 +69,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
|||
return null;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
|
|||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
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.states.StateDeveloper
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@ -55,7 +57,7 @@ class JSRequestExecutor {
|
|||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers, method, body);
|
||||
_executor.invokeV8("executeRequest", url, headers, method, body);
|
||||
} as V8Value;
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
|
@ -63,7 +65,7 @@ class JSRequestExecutor {
|
|||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers, method, body);
|
||||
_executor.invokeV8("executeRequest", url, headers, method, body);
|
||||
} as V8Value;
|
||||
|
||||
try {
|
||||
|
@ -110,7 +112,7 @@ class JSRequestExecutor {
|
|||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeVoid("cleanup", null);
|
||||
_executor.invokeV8("cleanup", null);
|
||||
};
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
|
@ -118,7 +120,7 @@ class JSRequestExecutor {
|
|||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeVoid("cleanup", null);
|
||||
_executor.invokeV8("cleanup", null);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
|||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
|
||||
class JSRequestModifier: IRequestModifier {
|
||||
private val _plugin: JSClient;
|
||||
|
@ -40,7 +42,7 @@ class JSRequestModifier: IRequestModifier {
|
|||
|
||||
return _plugin.busy {
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
|
||||
_modifier.invoke("modifyRequest", url, headers);
|
||||
_modifier.invokeV8("modifyRequest", url, headers);
|
||||
} as V8ValueObject;
|
||||
|
||||
val req = JSRequest(_plugin, result, url, headers);
|
||||
|
|
|
@ -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.getOrThrow
|
||||
import com.futo.platformplayer.getSourcePlugin
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -38,7 +39,7 @@ class JSSubtitleSource : ISubtitleSource {
|
|||
throw IllegalStateException("This subtitle doesn't support getSubtitles..");
|
||||
|
||||
return _obj.getSourcePlugin()?.busy {
|
||||
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
|
||||
val v8String = _obj.invokeV8<V8ValueString>("getSubtitles", arrayOf<Any>());
|
||||
return@busy v8String.value;
|
||||
} ?: "";
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
|
@ -86,7 +87,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||
private fun getPlaybackTrackerJS(): IPlaybackTracker? {
|
||||
return _plugin.busy {
|
||||
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;
|
||||
if(tracker is V8ValueObject)
|
||||
return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
|
||||
|
@ -111,7 +112,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +131,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||
|
||||
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
|
||||
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?
|
||||
return@busy null;
|
||||
|
||||
|
|
|
@ -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.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
|
||||
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
override val licenseUri: String
|
||||
|
@ -25,7 +27,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
|||
return null
|
||||
|
||||
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)
|
||||
|
|
|
@ -9,6 +9,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
|||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
|
||||
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||
IDashManifestWidevineSource, JSSource {
|
||||
|
@ -45,7 +47,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
|||
return null
|
||||
|
||||
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)
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.orNull
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
|
@ -64,7 +65,7 @@ abstract class JSSource {
|
|||
return@isBusyWith null;
|
||||
|
||||
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)
|
||||
|
@ -78,7 +79,7 @@ abstract class JSSource {
|
|||
|
||||
Logger.v("JSSource", "Request executor for [${type}] requesting");
|
||||
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");
|
||||
|
|
|
@ -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.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
|
||||
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
||||
override val licenseUri: String
|
||||
|
@ -25,7 +26,7 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
|||
return null
|
||||
|
||||
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)
|
||||
|
|
|
@ -64,6 +64,7 @@ import java.net.URLDecoder
|
|||
import java.net.URLEncoder
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class StateCasting {
|
||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||
|
@ -89,6 +90,7 @@ class StateCasting {
|
|||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||
private var _nsdManager: NsdManager? = null
|
||||
val isCasting: Boolean get() = activeDevice != null;
|
||||
private val _castId = AtomicInteger(0)
|
||||
|
||||
private val _discoveryListeners = mapOf(
|
||||
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
|
||||
|
@ -432,13 +434,19 @@ class StateCasting {
|
|||
action();
|
||||
}
|
||||
|
||||
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean {
|
||||
val ad = activeDevice ?: return false;
|
||||
fun cancel() {
|
||||
_castId.incrementAndGet()
|
||||
}
|
||||
|
||||
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 false;
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0;
|
||||
val castId = _castId.incrementAndGet()
|
||||
|
||||
var sourceCount = 0;
|
||||
if (videoSource != null) sourceCount++;
|
||||
|
@ -459,17 +467,11 @@ class StateCasting {
|
|||
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||
}
|
||||
} 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);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||
}
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
||||
} else {
|
||||
if (ad is FCastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as DASH direct");
|
||||
|
@ -482,10 +484,6 @@ class StateCasting {
|
|||
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;
|
||||
|
@ -526,24 +524,10 @@ class StateCasting {
|
|||
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);
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
@ -554,7 +538,8 @@ class StateCasting {
|
|||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return@withContext true;
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeVideo(): Boolean {
|
||||
|
@ -1236,7 +1221,7 @@ class StateCasting {
|
|||
}
|
||||
|
||||
@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();
|
||||
|
||||
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
|
||||
return@withContext if (audioSource != null && videoSource != null) {
|
||||
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
|
||||
val deferred = if (audioSource != null && videoSource != null) {
|
||||
JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO)
|
||||
} else if (audioSource != null) {
|
||||
audioSource.generate()
|
||||
audioSource.generateAsync(_scopeIO)
|
||||
} else if (videoSource != null) {
|
||||
videoSource.generate()
|
||||
videoSource.generateAsync(_scopeIO)
|
||||
} else {
|
||||
Logger.e(TAG, "Expected at least audio or video to be set")
|
||||
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")
|
||||
|
||||
if (castId != _castId.get()) {
|
||||
Log.i(TAG, "Get DASH cancelled.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
for (representation in representationRegex.findAll(dashContent)) {
|
||||
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
|
@ -668,7 +669,7 @@ class PackageHttp: V8Package {
|
|||
if(hasOpen && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeVoid("open", arrayOf<Any>());
|
||||
_listeners?.invokeV8Void("open", arrayOf<Any>());
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
|
@ -680,7 +681,7 @@ class PackageHttp: V8Package {
|
|||
if(hasMessage && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeVoid("message", msg);
|
||||
_listeners?.invokeV8Void("message", msg);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {}
|
||||
|
@ -691,7 +692,7 @@ class PackageHttp: V8Package {
|
|||
{
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeVoid("closing", code, reason);
|
||||
_listeners?.invokeV8Void("closing", code, reason);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
|
@ -704,7 +705,7 @@ class PackageHttp: V8Package {
|
|||
if(hasClosed && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeVoid("closed", code, reason);
|
||||
_listeners?.invokeV8Void("closed", code, reason);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
|
@ -722,7 +723,7 @@ class PackageHttp: V8Package {
|
|||
if(hasFailure && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeVoid("failure", exception.message);
|
||||
_listeners?.invokeV8Void("failure", exception.message);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.PictureInPictureParams
|
|||
import android.app.RemoteAction
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.platforms.js.JSClient
|
||||
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.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
|
@ -806,6 +809,8 @@ class VideoDetailView : ConstraintLayout {
|
|||
_lastVideoSource = null;
|
||||
_lastAudioSource = null;
|
||||
_lastSubtitleSource = null;
|
||||
_cast.cancel()
|
||||
StateCasting.instance.cancel()
|
||||
video = null;
|
||||
_container_content_liveChat?.close();
|
||||
_player.clear();
|
||||
|
@ -1898,11 +1903,46 @@ class VideoDetailView : ConstraintLayout {
|
|||
}
|
||||
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)")
|
||||
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?) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
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);
|
||||
} else throw IllegalStateException("Disconnected cast during loading");
|
||||
}
|
||||
}
|
||||
} 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
|
||||
|
@ -2415,7 +2455,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
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))
|
||||
_player.hideControls(false); //TODO: Disable player?
|
||||
|
||||
|
@ -2430,7 +2470,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
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))
|
||||
_player.hideControls(false); //TODO: Disable player?
|
||||
|
||||
|
@ -2446,7 +2486,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
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
|
||||
_player.swapSubtitles(fragment.lifecycleScope, toSet);
|
||||
|
||||
|
@ -2553,8 +2593,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
_cast.visibility = View.VISIBLE;
|
||||
} else {
|
||||
StateCasting.instance.stopVideo();
|
||||
_cast.stopTimeJob();
|
||||
_cast.visibility = View.GONE;
|
||||
_cast.cancel()
|
||||
|
||||
if (video?.isLive == false) {
|
||||
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
|
||||
|
|
|
@ -395,8 +395,9 @@ class StatePlatform {
|
|||
}
|
||||
suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
var removed: MutableList<IPlatformClient>;
|
||||
synchronized(_clientsLock) {
|
||||
val removed = _enabledClients.toMutableList();
|
||||
removed = _enabledClients.toMutableList();
|
||||
_enabledClients.clear();
|
||||
for (id in ids) {
|
||||
val client = getClient(id);
|
||||
|
@ -412,12 +413,12 @@ class StatePlatform {
|
|||
}
|
||||
_enabledClientsPersistent.set(*ids);
|
||||
_enabledClientsPersistent.save();
|
||||
}
|
||||
|
||||
for (oldClient in removed) {
|
||||
oldClient.disable();
|
||||
onSourceDisabled.emit(oldClient);
|
||||
}
|
||||
}
|
||||
afterLoad?.invoke();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -30,6 +30,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||
import com.futo.platformplayer.formatDuration
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -54,6 +55,7 @@ class CastView : ConstraintLayout {
|
|||
private val _timeBar: DefaultTimeBar;
|
||||
private val _background: FrameLayout;
|
||||
private val _gestureControlView: GestureControlView;
|
||||
private val _loaderGame: TargetTapLoaderView
|
||||
private var _scope: CoroutineScope = CoroutineScope(Dispatchers.Main);
|
||||
private var _updateTimeJob: Job? = null;
|
||||
private var _inPictureInPicture: Boolean = false;
|
||||
|
@ -88,6 +90,9 @@ class CastView : ConstraintLayout {
|
|||
_timeBar = findViewById(R.id.time_progress);
|
||||
_background = findViewById(R.id.layout_background);
|
||||
_gestureControlView = findViewById(R.id.gesture_control);
|
||||
_loaderGame = findViewById(R.id.loader_overlay)
|
||||
_loaderGame.visibility = View.GONE
|
||||
|
||||
_gestureControlView.fullScreenGestureEnabled = false
|
||||
_gestureControlView.setupTouchArea();
|
||||
_gestureControlView.onSpeedHoldStart.subscribe {
|
||||
|
@ -197,6 +202,12 @@ class CastView : ConstraintLayout {
|
|||
_updateTimeJob = null;
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
stopTimeJob()
|
||||
setLoading(false)
|
||||
visibility = View.GONE
|
||||
}
|
||||
|
||||
fun stopAllGestures() {
|
||||
_gestureControlView.stopAllGestures();
|
||||
}
|
||||
|
@ -279,6 +290,7 @@ class CastView : ConstraintLayout {
|
|||
_textDuration.text = (video.duration * 1000).formatDuration();
|
||||
_timeBar.setPosition(position);
|
||||
_timeBar.setDuration(video.duration);
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
|
@ -295,6 +307,7 @@ class CastView : ConstraintLayout {
|
|||
_updateTimeJob?.cancel();
|
||||
_updateTimeJob = null;
|
||||
_scope.cancel();
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
private fun getPlaybackStateCompat(): Int {
|
||||
|
@ -305,4 +318,19 @@ class CastView : ConstraintLayout {
|
|||
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())
|
||||
}
|
||||
}
|
|
@ -64,6 +64,7 @@ class LiveChatOverlay : LinearLayout {
|
|||
|
||||
private val _overlayRaid: ConstraintLayout;
|
||||
private val _overlayRaid_Name: TextView;
|
||||
private val _overlayRaid_Message: TextView;
|
||||
private val _overlayRaid_Thumbnail: ImageView;
|
||||
|
||||
private val _overlayRaid_ButtonGo: Button;
|
||||
|
@ -146,6 +147,7 @@ class LiveChatOverlay : LinearLayout {
|
|||
|
||||
_overlayRaid = findViewById(R.id.overlay_raid);
|
||||
_overlayRaid_Name = findViewById(R.id.raid_name);
|
||||
_overlayRaid_Message = findViewById(R.id.textRaidMessage);
|
||||
_overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail);
|
||||
_overlayRaid_ButtonGo = findViewById(R.id.raid_button_go);
|
||||
_overlayRaid_ButtonDismiss = findViewById(R.id.raid_button_prevent);
|
||||
|
@ -371,7 +373,14 @@ class LiveChatOverlay : LinearLayout {
|
|||
else
|
||||
_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) {
|
||||
|
|
|
@ -44,6 +44,7 @@ import com.futo.platformplayer.formatDuration
|
|||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -154,10 +155,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||
|
||||
val onChapterClicked = Event1<IChapter>();
|
||||
|
||||
private val loaderOverlay: FrameLayout
|
||||
private val loaderIndeterminate: android.widget.ProgressBar
|
||||
private val loaderDeterminate: android.widget.ProgressBar
|
||||
private var determinateAnimator: ValueAnimator? = null
|
||||
private val _loaderGame: TargetTapLoaderView
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
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_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause);
|
||||
|
||||
loaderOverlay = findViewById(R.id.loader_overlay)
|
||||
loaderIndeterminate = findViewById(R.id.loader_indeterminate)
|
||||
loaderDeterminate = findViewById(R.id.loader_determinate)
|
||||
|
||||
loaderOverlay.visibility = View.GONE
|
||||
loaderIndeterminate.visibility = View.GONE
|
||||
loaderDeterminate.visibility = View.GONE
|
||||
_loaderGame = findViewById(R.id.loader_overlay)
|
||||
_loaderGame.visibility = View.GONE
|
||||
|
||||
_control_chapter.setOnClickListener {
|
||||
_currentChapter?.let {
|
||||
|
@ -884,33 +877,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||
}
|
||||
|
||||
override fun setLoading(isLoading: Boolean) {
|
||||
determinateAnimator?.cancel()
|
||||
if (isLoading) {
|
||||
loaderOverlay.visibility = View.VISIBLE
|
||||
loaderIndeterminate.visibility = View.VISIBLE
|
||||
loaderDeterminate.visibility = View.GONE
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader()
|
||||
} else {
|
||||
loaderOverlay.visibility = View.GONE
|
||||
loaderIndeterminate.visibility = View.GONE
|
||||
loaderDeterminate.visibility = View.GONE
|
||||
_loaderGame.visibility = View.GONE
|
||||
_loaderGame.stopAndResetLoader()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setLoading(expectedDurationMs: Int) {
|
||||
determinateAnimator?.cancel()
|
||||
|
||||
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()
|
||||
}
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader(expectedDurationMs.toLong())
|
||||
}
|
||||
}
|
|
@ -3,11 +3,13 @@ package com.futo.platformplayer.views.video
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
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.trackselection.DefaultTrackSelector
|
||||
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.streams.VideoMuxedSourceDescriptor
|
||||
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.JSSource
|
||||
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.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.fragment.mainactivity.main.ChannelFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
|
@ -72,6 +81,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
@ -116,7 +126,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
|
||||
private var _didCallSourceChange = false;
|
||||
private var _lastState: Int = -1;
|
||||
|
||||
private val _swapIdAudio = AtomicInteger(0)
|
||||
private val _swapIdVideo = AtomicInteger(0)
|
||||
|
||||
var targetTrackVideoHeight = -1
|
||||
private set
|
||||
|
@ -435,13 +446,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
|
||||
|
||||
private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean {
|
||||
setLoading(false)
|
||||
val swapId = _swapIdVideo.incrementAndGet()
|
||||
_lastGeneratedDash = null;
|
||||
val didSet = when(videoSource) {
|
||||
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
|
||||
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
|
||||
is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(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 IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
|
||||
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
||||
|
@ -452,11 +465,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
return didSet;
|
||||
}
|
||||
private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean {
|
||||
setLoading(false)
|
||||
val swapId = _swapIdAudio.incrementAndGet()
|
||||
val didSet = when(audioSource) {
|
||||
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
|
||||
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(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 IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
|
||||
null -> { _lastAudioMediaSource = null; true; }
|
||||
|
@ -563,7 +578,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
}.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
}
|
||||
@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]");
|
||||
|
||||
if(videoSource.hasGenerate) {
|
||||
|
@ -582,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
}
|
||||
}
|
||||
val generated = generatedDef.await();
|
||||
if (_swapIdVideo.get() != swapId) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(false)
|
||||
}
|
||||
|
@ -707,7 +726,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
}
|
||||
|
||||
@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]");
|
||||
if(audioSource.hasGenerate) {
|
||||
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
|
||||
|
@ -725,6 +744,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
}
|
||||
}
|
||||
val generated = generatedDef.await();
|
||||
if (_swapIdAudio.get() != swapId) {
|
||||
return@launch
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(false)
|
||||
}
|
||||
|
@ -888,6 +910,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
fun clear() {
|
||||
exoPlayer?.player?.stop();
|
||||
exoPlayer?.player?.clearMediaItems();
|
||||
setLoading(false)
|
||||
_swapIdVideo.incrementAndGet()
|
||||
_swapIdAudio.incrementAndGet()
|
||||
_lastVideoMediaSource = null;
|
||||
_lastAudioMediaSource = null;
|
||||
_lastSubtitleMediaSource = null;
|
||||
|
|
40
app/src/main/res/drawable/progress_bar.xml
Normal file
40
app/src/main/res/drawable/progress_bar.xml
Normal 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>
|
|
@ -5,9 +5,8 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
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_height="200dp"
|
||||
app:progress="0%"
|
||||
app:strokeWidth="20dp" />
|
||||
android:layout_height="240dp" />
|
||||
</FrameLayout>
|
|
@ -65,35 +65,10 @@
|
|||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
<com.futo.platformplayer.views.TargetTapLoaderView
|
||||
android:id="@+id/loader_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="6dp"
|
||||
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>
|
|
@ -189,4 +189,14 @@
|
|||
app:layout_constraintLeft_toLeftOf="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>
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue