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
|
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));
|
||||||
}
|
}
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
} ?: "";
|
} ?: "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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){
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.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())
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
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"
|
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>
|
|
@ -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>
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue