mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01cb544dfd | |||
| b9239b6177 | |||
| 96ca3f62a2 | |||
| 73ad783881 | |||
| 3bfcf65535 | |||
| 8b3b27a2a8 | |||
| a4d4835a89 | |||
| 56c0f7bfaf | |||
| 736424ae35 | |||
| 37dc778009 | |||
| cd3cea58a4 | |||
| 8b53e9e5e3 | |||
| 08e98b089c | |||
| 5528d71da8 | |||
| 83f520ca44 | |||
| cc247ce634 |
@@ -32,7 +32,6 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.whenStateAtLeast
|
||||
import androidx.lifecycle.withStateAtLeast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
@@ -114,7 +113,6 @@ import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
@@ -610,6 +608,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
)
|
||||
}
|
||||
|
||||
//startActivity(Intent(this, TestActivity::class.java))
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -2,12 +2,24 @@ package com.futo.platformplayer.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TestActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_test);
|
||||
|
||||
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
|
||||
view.startLoader(10000)
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(5000)
|
||||
view.startLoader()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+3
-2
@@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||
@@ -85,12 +86,12 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
|
||||
}
|
||||
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
@@ -60,7 +61,7 @@ class JSComment : IPlatformComment {
|
||||
if(!_hasGetReplies)
|
||||
return null;
|
||||
|
||||
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
|
||||
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
|
||||
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
|
||||
return JSCommentPager(_config!!, plugin, obj);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ abstract class JSPager<T> : IPager<T> {
|
||||
}
|
||||
|
||||
override fun hasMorePages(): Boolean {
|
||||
return _hasMorePages;
|
||||
return _hasMorePages && !pager.isClosed;
|
||||
}
|
||||
|
||||
override fun nextPage() {
|
||||
|
||||
+3
-2
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
@@ -68,12 +69,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
return null;
|
||||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getSourcePlugin
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -38,7 +39,7 @@ class JSSubtitleSource : ISubtitleSource {
|
||||
throw IllegalStateException("This subtitle doesn't support getSubtitles..");
|
||||
|
||||
return _obj.getSourcePlugin()?.busy {
|
||||
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
|
||||
val v8String = _obj.invokeV8<V8ValueString>("getSubtitles", arrayOf<Any>());
|
||||
return@busy v8String.value;
|
||||
} ?: "";
|
||||
}
|
||||
|
||||
+4
-3
@@ -24,6 +24,7 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
@@ -86,7 +87,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
private fun getPlaybackTrackerJS(): IPlaybackTracker? {
|
||||
return _plugin.busy {
|
||||
V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
|
||||
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>())
|
||||
val tracker = _content.invokeV8<V8Value>("getPlaybackTracker", arrayOf<Any>())
|
||||
?: return@catchScriptErrors null;
|
||||
if(tracker is V8ValueObject)
|
||||
return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
|
||||
@@ -111,7 +112,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
return _plugin.busy {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return@busy JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
}
|
||||
@@ -130,7 +131,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
|
||||
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
|
||||
return _plugin.busy {
|
||||
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
|
||||
val commentPager = _content.invokeV8<V8Value>("getComments", arrayOf<Any>());
|
||||
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
|
||||
return@busy null;
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class StateCasting {
|
||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||
@@ -89,6 +90,7 @@ class StateCasting {
|
||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||
private var _nsdManager: NsdManager? = null
|
||||
val isCasting: Boolean get() = activeDevice != null;
|
||||
private val _castId = AtomicInteger(0)
|
||||
|
||||
private val _discoveryListeners = mapOf(
|
||||
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
|
||||
@@ -432,129 +434,112 @@ class StateCasting {
|
||||
action();
|
||||
}
|
||||
|
||||
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean {
|
||||
val ad = activeDevice ?: return false;
|
||||
if (ad.connectionState != CastConnectionState.CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
fun cancel() {
|
||||
_castId.incrementAndGet()
|
||||
}
|
||||
|
||||
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;
|
||||
if (videoSource != null) sourceCount++;
|
||||
if (audioSource != null) sourceCount++;
|
||||
if (subtitleSource != null) sourceCount++;
|
||||
val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0;
|
||||
val castId = _castId.incrementAndGet()
|
||||
|
||||
if (sourceCount < 1) {
|
||||
throw Exception("At least one source should be specified.");
|
||||
}
|
||||
var sourceCount = 0;
|
||||
if (videoSource != null) sourceCount++;
|
||||
if (audioSource != null) sourceCount++;
|
||||
if (subtitleSource != null) sourceCount++;
|
||||
|
||||
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);
|
||||
if (sourceCount < 1) {
|
||||
throw Exception("At least one source should be specified.");
|
||||
}
|
||||
|
||||
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 {
|
||||
Logger.i(TAG, "Casting as local DASH");
|
||||
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||
}
|
||||
} else {
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||
if (isRawDash) {
|
||||
Logger.i(TAG, "Casting as raw DASH");
|
||||
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||
if (isRawDash) {
|
||||
Logger.i(TAG, "Casting as raw DASH");
|
||||
|
||||
try {
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||
}
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
||||
} else {
|
||||
if (ad is FCastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as DASH direct");
|
||||
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 {
|
||||
if (ad is FCastCastingDevice) {
|
||||
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 {
|
||||
Logger.i(TAG, "Casting as DASH indirect");
|
||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
}
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
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 {
|
||||
@@ -1236,7 +1221,7 @@ class StateCasting {
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?, castId: Int, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
cleanExecutors()
|
||||
@@ -1283,20 +1268,48 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
|
||||
var dashContent = withContext(Dispatchers.IO) {
|
||||
var dashContent: String = withContext(Dispatchers.IO) {
|
||||
stopVideo()
|
||||
|
||||
//TODO: Include subtitlesURl in the future
|
||||
return@withContext if (audioSource != null && videoSource != null) {
|
||||
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
|
||||
val deferred = if (audioSource != null && videoSource != null) {
|
||||
JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO)
|
||||
} else if (audioSource != null) {
|
||||
audioSource.generate()
|
||||
audioSource.generateAsync(_scopeIO)
|
||||
} else if (videoSource != null) {
|
||||
videoSource.generate()
|
||||
videoSource.generateAsync(_scopeIO)
|
||||
} else {
|
||||
Logger.e(TAG, "Expected at least audio or video to be set")
|
||||
null
|
||||
}
|
||||
|
||||
if (deferred != null) {
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (deferred.estDuration >= 0) {
|
||||
onLoadingEstimate?.invoke(deferred.estDuration)
|
||||
} else {
|
||||
onLoading?.invoke(true)
|
||||
}
|
||||
}
|
||||
deferred.await()
|
||||
} finally {
|
||||
if (castId == _castId.get()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
onLoading?.invoke(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return@withContext null
|
||||
}
|
||||
} ?: throw Exception("Dash is null")
|
||||
|
||||
if (castId != _castId.get()) {
|
||||
Log.i(TAG, "Get DASH cancelled.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
for (representation in representationRegex.findAll(dashContent)) {
|
||||
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||
|
||||
+48
-9
@@ -4,6 +4,7 @@ import android.app.PictureInPictureParams
|
||||
import android.app.RemoteAction
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
@@ -79,7 +80,9 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
@@ -806,6 +809,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_lastVideoSource = null;
|
||||
_lastAudioSource = null;
|
||||
_lastSubtitleSource = null;
|
||||
_cast.cancel()
|
||||
StateCasting.instance.cancel()
|
||||
video = null;
|
||||
_container_content_liveChat?.close();
|
||||
_player.clear();
|
||||
@@ -1898,11 +1903,46 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
|
||||
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
|
||||
castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)
|
||||
}
|
||||
|
||||
if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)) {
|
||||
_cast.setVideoDetails(video, resumePositionMs / 1000);
|
||||
setCastEnabled(true);
|
||||
} else throw IllegalStateException("Disconnected cast during loading");
|
||||
private fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
|
||||
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
|
||||
else null
|
||||
|
||||
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
|
||||
try {
|
||||
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
||||
_cast.setLoading(it)
|
||||
}, onLoadingEstimate = {
|
||||
_cast.setLoading(it)
|
||||
})
|
||||
|
||||
if (castingSucceeded) {
|
||||
withContext(Dispatchers.Main) {
|
||||
_cast.setVideoDetails(video, resumePositionMs / 1000);
|
||||
setCastEnabled(true);
|
||||
}
|
||||
}
|
||||
} catch (e: ScriptReloadRequiredException) {
|
||||
Log.i(TAG, "Reload required exception", e)
|
||||
if (plugin == null)
|
||||
throw e
|
||||
|
||||
if (startId != -1 && plugin.getUnderlyingPlugin().runtimeId != startId)
|
||||
throw e
|
||||
|
||||
StatePlatform.instance.handleReloadRequired(e, {
|
||||
fetchVideo()
|
||||
});
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "loadCurrentVideoCast", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Events
|
||||
@@ -2415,7 +2455,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||
StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||
castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
|
||||
_player.hideControls(false); //TODO: Disable player?
|
||||
|
||||
@@ -2430,7 +2470,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||
castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
|
||||
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
||||
_player.hideControls(false); //TODO: Disable player?
|
||||
|
||||
@@ -2446,7 +2486,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||
castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||
else
|
||||
_player.swapSubtitles(fragment.lifecycleScope, toSet);
|
||||
|
||||
@@ -2553,8 +2593,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_cast.visibility = View.VISIBLE;
|
||||
} else {
|
||||
StateCasting.instance.stopVideo();
|
||||
_cast.stopTimeJob();
|
||||
_cast.visibility = View.GONE;
|
||||
_cast.cancel()
|
||||
|
||||
if (video?.isLive == false) {
|
||||
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
|
||||
|
||||
@@ -395,8 +395,9 @@ class StatePlatform {
|
||||
}
|
||||
suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
var removed: MutableList<IPlatformClient>;
|
||||
synchronized(_clientsLock) {
|
||||
val removed = _enabledClients.toMutableList();
|
||||
removed = _enabledClients.toMutableList();
|
||||
_enabledClients.clear();
|
||||
for (id in ids) {
|
||||
val client = getClient(id);
|
||||
@@ -412,11 +413,11 @@ class StatePlatform {
|
||||
}
|
||||
_enabledClientsPersistent.set(*ids);
|
||||
_enabledClientsPersistent.save();
|
||||
}
|
||||
|
||||
for (oldClient in removed) {
|
||||
oldClient.disable();
|
||||
onSourceDisabled.emit(oldClient);
|
||||
}
|
||||
for (oldClient in removed) {
|
||||
oldClient.disable();
|
||||
onSourceDisabled.emit(oldClient);
|
||||
}
|
||||
afterLoad?.invoke();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.toColorInt
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
|
||||
class TargetTapLoaderView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : View(context, attrs) {
|
||||
private val primaryColor = "#2D63ED".toColorInt()
|
||||
private val inactiveGlobalAlpha = 110
|
||||
private val idleSpeedMultiplier = .015f
|
||||
private val overshootInterpolator = OvershootInterpolator(1.5f)
|
||||
private val floatAccel = .03f
|
||||
private val idleMaxSpeed = .35f
|
||||
private val idleInitialTargets = 10
|
||||
private val idleHintText = "Waiting for media to become available"
|
||||
|
||||
private var expectedDurationMs: Long? = null
|
||||
private var loadStartTime = 0L
|
||||
private var playStartTime = 0L
|
||||
private var loaderFinished = false
|
||||
private var forceIndeterminate= false
|
||||
private var lastFrameTime = System.currentTimeMillis()
|
||||
|
||||
private var score = 0
|
||||
private var isPlaying = false
|
||||
|
||||
private val targets = mutableListOf<Target>()
|
||||
private val particles = mutableListOf<Particle>()
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.argb(0.7f, 1f, 1f, 1f)
|
||||
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics)
|
||||
textAlign = Paint.Align.LEFT
|
||||
setShadowLayer(4f, 0f, 0f, Color.BLACK)
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
private val idleHintPaint = Paint(textPaint).apply {
|
||||
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics)
|
||||
typeface = Typeface.DEFAULT
|
||||
setShadowLayer(2f, 0f, 0f, Color.BLACK)
|
||||
}
|
||||
private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = primaryColor }
|
||||
private val spinnerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = primaryColor; strokeWidth = 12f
|
||||
style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.argb(50, 0, 0, 0) }
|
||||
private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val backgroundPaint = Paint()
|
||||
private var spinnerShader: SweepGradient? = null
|
||||
private var spinnerAngle = 0f
|
||||
private val MIN_SPAWN_RATE = 1f
|
||||
private val MAX_SPAWN_RATE = 20.0f
|
||||
private val HIT_RATE_INCREMENT = 0.15f
|
||||
private val MISS_RATE_DECREMENT = 0.09f
|
||||
private var spawnRate = MIN_SPAWN_RATE
|
||||
|
||||
private val frameRunnable = object : Runnable {
|
||||
override fun run() { invalidate(); if (!loaderFinished) postDelayed(this, 16L) }
|
||||
}
|
||||
|
||||
init { setOnTouchListener { _, e -> if (e.action == MotionEvent.ACTION_DOWN) handleTap(e.x, e.y); true } }
|
||||
|
||||
fun startLoader(durationMs: Long? = null) {
|
||||
val alreadyRunning = !loaderFinished
|
||||
if (alreadyRunning && durationMs == null) {
|
||||
expectedDurationMs = null
|
||||
forceIndeterminate = true
|
||||
return
|
||||
}
|
||||
|
||||
expectedDurationMs = durationMs?.takeIf { it > 0 }
|
||||
forceIndeterminate = expectedDurationMs == null
|
||||
loaderFinished = false
|
||||
isPlaying = false
|
||||
score = 0
|
||||
particles.clear()
|
||||
spawnRate = MIN_SPAWN_RATE
|
||||
|
||||
post { if (targets.isEmpty()) prepopulateIdleTargets() }
|
||||
|
||||
loadStartTime = System.currentTimeMillis()
|
||||
playStartTime = 0
|
||||
removeCallbacks(frameRunnable)
|
||||
post(frameRunnable)
|
||||
|
||||
if (!isIndeterminate) {
|
||||
postDelayed({
|
||||
if (!loaderFinished) {
|
||||
forceIndeterminate = true
|
||||
expectedDurationMs = null
|
||||
}
|
||||
}, expectedDurationMs!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun finishLoader() {
|
||||
loaderFinished = true
|
||||
particles.clear()
|
||||
isPlaying = false
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun stopAndResetLoader() {
|
||||
if (score > 0) {
|
||||
val elapsed = (System.currentTimeMillis() - (if (playStartTime > 0) playStartTime else loadStartTime)) / 1000.0
|
||||
UIDialogs.toast("Nice! score $score | ${"%.1f".format(score / elapsed)} / s")
|
||||
score = 0
|
||||
}
|
||||
loaderFinished = true
|
||||
isPlaying = false
|
||||
targets.clear()
|
||||
particles.clear()
|
||||
removeCallbacks(frameRunnable)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private val isIndeterminate get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L
|
||||
|
||||
private fun handleTap(x: Float, y: Float) {
|
||||
val idx = targets.indexOfFirst { !it.hit && hypot(x - it.x, y - it.y) <= it.radius }
|
||||
if (idx >= 0) {
|
||||
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
val t = targets[idx]
|
||||
t.hit = true; t.hitTime = System.currentTimeMillis()
|
||||
accelerateSpawnRate()
|
||||
score += if (!isIndeterminate) 10 else 5
|
||||
spawnParticles(t.x, t.y, t.radius)
|
||||
|
||||
if (!isPlaying) {
|
||||
isPlaying = true
|
||||
playStartTime = System.currentTimeMillis()
|
||||
score = 0
|
||||
spawnRate = MIN_SPAWN_RATE
|
||||
targets.retainAll { it === t }
|
||||
spawnTarget()
|
||||
}
|
||||
} else if (isPlaying) decelerateSpawnRate()
|
||||
}
|
||||
|
||||
private inline fun accelerateSpawnRate() {
|
||||
spawnRate = (spawnRate + HIT_RATE_INCREMENT).coerceAtMost(MAX_SPAWN_RATE)
|
||||
}
|
||||
|
||||
private inline fun decelerateSpawnRate() {
|
||||
spawnRate = (spawnRate - MISS_RATE_DECREMENT).coerceAtLeast(MIN_SPAWN_RATE)
|
||||
}
|
||||
|
||||
private fun spawnTarget() {
|
||||
if (loaderFinished || width == 0 || height == 0) {
|
||||
postDelayed({ spawnTarget() }, 200L); return
|
||||
}
|
||||
|
||||
if (!isPlaying) {
|
||||
postDelayed({ spawnTarget() }, 500L); return
|
||||
}
|
||||
|
||||
val radius = Random.nextInt(40, 80).toFloat()
|
||||
val x = Random.nextFloat() * (width - 2 * radius) + radius
|
||||
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
|
||||
|
||||
val baseSpeed = Random.nextFloat() + .1f
|
||||
val speed = baseSpeed
|
||||
val angle = Random.nextFloat() * TAU
|
||||
val vx = cos(angle) * speed
|
||||
val vy = sin(angle) * speed
|
||||
val alpha = Random.nextInt(150, 255)
|
||||
|
||||
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
|
||||
|
||||
val delay = (1000f / spawnRate).roundToLong()
|
||||
postDelayed({ spawnTarget() }, delay)
|
||||
}
|
||||
|
||||
private fun prepopulateIdleTargets() {
|
||||
if (width == 0 || height == 0) {
|
||||
post { prepopulateIdleTargets() }
|
||||
return
|
||||
}
|
||||
repeat(idleInitialTargets) {
|
||||
val radius = Random.nextInt(40, 80).toFloat()
|
||||
val x = Random.nextFloat() * (width - 2 * radius) + radius
|
||||
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
|
||||
val angle = Random.nextFloat() * TAU
|
||||
val speed = (Random.nextFloat() * .3f + .05f) * idleSpeedMultiplier
|
||||
val vx = cos(angle) * speed
|
||||
val vy = sin(angle) * speed
|
||||
val alpha = Random.nextInt(60, 110)
|
||||
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawnParticles(cx: Float, cy: Float, radius: Float) {
|
||||
repeat(12) {
|
||||
val angle = Random.nextFloat() * TAU
|
||||
val speed = Random.nextFloat() * 5f + 2f
|
||||
val vx = cos(angle) * speed
|
||||
val vy = sin(angle) * speed
|
||||
val col = ColorUtils.setAlphaComponent(primaryColor, Random.nextInt(120, 255))
|
||||
particles += Particle(cx, cy, vx, vy, System.currentTimeMillis(), col)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val deltaMs = now - lastFrameTime
|
||||
lastFrameTime = now
|
||||
|
||||
drawBackground(canvas)
|
||||
drawTargets(canvas, now)
|
||||
drawParticles(canvas, now)
|
||||
|
||||
if (!loaderFinished) {
|
||||
if (isIndeterminate) drawIndeterminateSpinner(canvas, deltaMs)
|
||||
else drawDeterministicProgressBar(canvas, now)
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
val margin = 24f
|
||||
val scoreTxt = "Score: $score"
|
||||
val speedTxt = "Speed: ${"%.2f".format(spawnRate)}/s"
|
||||
val maxWidth = width - margin
|
||||
val needRight = max(textPaint.measureText(scoreTxt), textPaint.measureText(speedTxt)) > maxWidth
|
||||
|
||||
val alignX = if (needRight) (width - margin) else margin
|
||||
textPaint.textAlign = if (needRight) Paint.Align.RIGHT else Paint.Align.LEFT
|
||||
|
||||
canvas.drawText(scoreTxt, alignX, textPaint.textSize + margin, textPaint)
|
||||
canvas.drawText(speedTxt, alignX, 2*textPaint.textSize + margin + 4f, textPaint)
|
||||
textPaint.textAlign = Paint.Align.LEFT
|
||||
}
|
||||
else if (loaderFinished)
|
||||
canvas.drawText("Loading Complete!", width/2f, height/2f, textPaint.apply { textAlign = Paint.Align.CENTER })
|
||||
else {
|
||||
idleHintPaint.textAlign = Paint.Align.CENTER
|
||||
canvas.drawText(idleHintText, width / 2f, height - 48f, idleHintPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawBackground(canvas: Canvas) {
|
||||
val colors = intArrayOf(
|
||||
Color.rgb(20, 20, 40),
|
||||
Color.rgb(15, 15, 30),
|
||||
Color.rgb(10, 10, 20),
|
||||
Color.rgb( 5, 5, 10),
|
||||
Color.BLACK
|
||||
)
|
||||
val pos = floatArrayOf(0f, 0.25f, 0.5f, 0.75f, 1f)
|
||||
|
||||
if (backgroundPaint.shader == null) {
|
||||
backgroundPaint.shader = LinearGradient(
|
||||
0f, 0f, 0f, height.toFloat(),
|
||||
colors, pos,
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
}
|
||||
|
||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint)
|
||||
}
|
||||
|
||||
private fun drawTargets(canvas: Canvas, now: Long) {
|
||||
val expireMsActive = if (isIndeterminate) 2500L else 1500L
|
||||
val it = targets.iterator()
|
||||
while (it.hasNext()) {
|
||||
val t = it.next()
|
||||
if (t.hit && now - t.hitTime > 300L) { it.remove(); continue }
|
||||
if (isPlaying && !t.hit && now - t.spawnTime > expireMsActive) {
|
||||
it.remove(); decelerateSpawnRate(); continue
|
||||
}
|
||||
t.x += t.vx; t.y += t.vy
|
||||
t.vx += (Random.nextFloat() - .5f) * floatAccel
|
||||
t.vy += (Random.nextFloat() - .5f) * floatAccel
|
||||
val speedCap = if (isPlaying) Float.MAX_VALUE else idleMaxSpeed
|
||||
val mag = hypot(t.vx, t.vy)
|
||||
if (mag > speedCap) {
|
||||
val s = speedCap / mag
|
||||
t.vx *= s; t.vy *= s
|
||||
}
|
||||
if (t.x - t.radius < 0 || t.x + t.radius > width) t.vx *= -1
|
||||
if (t.y - t.radius < 0 || t.y + t.radius > height) t.vy *= -1
|
||||
val scale = if (t.hit) 1f - ((now - t.hitTime) / 300f).coerceIn(0f,1f)
|
||||
else {
|
||||
val e = now - t.spawnAnimStart
|
||||
if (e < 300L) overshootInterpolator.getInterpolation(e/300f)
|
||||
else 1f + .02f * sin(((now - t.spawnAnimStart)/1000f)*TAU + t.pulseOffset)
|
||||
}
|
||||
val animAlpha = if (t.hit) ((1f - scale)*255).toInt() else 255
|
||||
val globalAlpha = if (isPlaying) 255 else inactiveGlobalAlpha
|
||||
val alpha = (animAlpha * t.baseAlpha /255f * globalAlpha/255f).toInt().coerceAtMost(255)
|
||||
val r = max(1f, t.radius*scale)
|
||||
val outerCol = ColorUtils.setAlphaComponent(primaryColor, alpha)
|
||||
val midCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.7f).toInt())
|
||||
val innerCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.4f).toInt())
|
||||
outerRingPaint.color = outerCol; middleRingPaint.color = midCol; centerDotPaint.color = innerCol
|
||||
|
||||
glowPaint.shader = RadialGradient(t.x, t.y, r, outerCol, Color.TRANSPARENT, Shader.TileMode.CLAMP)
|
||||
|
||||
canvas.drawCircle(t.x, t.y, r*1.2f, glowPaint)
|
||||
canvas.drawCircle(t.x+4f, t.y+4f, r, shadowPaint)
|
||||
canvas.drawCircle(t.x, t.y, r, outerRingPaint)
|
||||
canvas.drawCircle(t.x, t.y, r*.66f, middleRingPaint)
|
||||
canvas.drawCircle(t.x, t.y, r*.33f, centerDotPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawParticles(canvas: Canvas, now: Long) {
|
||||
val lifespan = 400L
|
||||
val it = particles.iterator()
|
||||
while (it.hasNext()) {
|
||||
val p = it.next()
|
||||
val age = now - p.startTime
|
||||
if (age > lifespan) { it.remove(); continue }
|
||||
val a = ((1f - age/lifespan.toFloat())*255).toInt()
|
||||
particlePaint.color = ColorUtils.setAlphaComponent(p.baseColor, a)
|
||||
p.x += p.vx; p.y += p.vy
|
||||
canvas.drawCircle(p.x, p.y, 6f, particlePaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawDeterministicProgressBar(canvas: Canvas, now: Long) {
|
||||
val dur = expectedDurationMs ?: return
|
||||
val prog = ((now - loadStartTime) / dur.toFloat()).coerceIn(0f, 1f)
|
||||
val eased = AccelerateDecelerateInterpolator().getInterpolation(prog)
|
||||
val h = 20f; val r=10f
|
||||
canvas.drawRoundRect(RectF(0f, height-h, width*eased, height.toFloat()), r, r, progressBarPaint)
|
||||
}
|
||||
|
||||
private fun drawIndeterminateSpinner(canvas: Canvas, dt: Long) {
|
||||
val cx=width/2f; val cy=height/2f; val r=min(width,height)/6f
|
||||
spinnerAngle = (spinnerAngle + .25f*dt)%360f
|
||||
if(spinnerShader == null) spinnerShader = SweepGradient(cx,cy,intArrayOf(Color.TRANSPARENT,Color.WHITE,Color.TRANSPARENT),floatArrayOf(0f,.5f,1f))
|
||||
spinnerPaint.shader = spinnerShader
|
||||
val glow = Paint(spinnerPaint).apply{ maskFilter = BlurMaskFilter(15f,BlurMaskFilter.Blur.SOLID) }
|
||||
val sweep = 270f
|
||||
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,glow)
|
||||
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,spinnerPaint)
|
||||
}
|
||||
|
||||
private data class Target(
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
val radius: Float,
|
||||
val spawnTime: Long,
|
||||
var hit: Boolean = false,
|
||||
var hitTime: Long = 0L,
|
||||
val baseAlpha: Int = 255,
|
||||
var vx: Float=0f,
|
||||
var vy:Float=0f,
|
||||
val spawnAnimStart: Long = System.currentTimeMillis(),
|
||||
val pulseOffset: Float = Random.nextFloat() * TAU
|
||||
)
|
||||
private data class Particle(
|
||||
var x:Float,
|
||||
var y:Float,
|
||||
val vx:Float,
|
||||
val vy:Float,
|
||||
val startTime:Long,
|
||||
val baseColor:Int
|
||||
)
|
||||
|
||||
private companion object { private const val TAU = (2 * Math.PI).toFloat() }
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.formatDuration
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -54,6 +55,7 @@ class CastView : ConstraintLayout {
|
||||
private val _timeBar: DefaultTimeBar;
|
||||
private val _background: FrameLayout;
|
||||
private val _gestureControlView: GestureControlView;
|
||||
private val _loaderGame: TargetTapLoaderView
|
||||
private var _scope: CoroutineScope = CoroutineScope(Dispatchers.Main);
|
||||
private var _updateTimeJob: Job? = null;
|
||||
private var _inPictureInPicture: Boolean = false;
|
||||
@@ -88,6 +90,9 @@ class CastView : ConstraintLayout {
|
||||
_timeBar = findViewById(R.id.time_progress);
|
||||
_background = findViewById(R.id.layout_background);
|
||||
_gestureControlView = findViewById(R.id.gesture_control);
|
||||
_loaderGame = findViewById(R.id.loader_overlay)
|
||||
_loaderGame.visibility = View.GONE
|
||||
|
||||
_gestureControlView.fullScreenGestureEnabled = false
|
||||
_gestureControlView.setupTouchArea();
|
||||
_gestureControlView.onSpeedHoldStart.subscribe {
|
||||
@@ -197,6 +202,12 @@ class CastView : ConstraintLayout {
|
||||
_updateTimeJob = null;
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
stopTimeJob()
|
||||
setLoading(false)
|
||||
visibility = View.GONE
|
||||
}
|
||||
|
||||
fun stopAllGestures() {
|
||||
_gestureControlView.stopAllGestures();
|
||||
}
|
||||
@@ -279,6 +290,7 @@ class CastView : ConstraintLayout {
|
||||
_textDuration.text = (video.duration * 1000).formatDuration();
|
||||
_timeBar.setPosition(position);
|
||||
_timeBar.setDuration(video.duration);
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@@ -295,6 +307,7 @@ class CastView : ConstraintLayout {
|
||||
_updateTimeJob?.cancel();
|
||||
_updateTimeJob = null;
|
||||
_scope.cancel();
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
private fun getPlaybackStateCompat(): Int {
|
||||
@@ -305,4 +318,19 @@ class CastView : ConstraintLayout {
|
||||
else -> PlaybackStateCompat.STATE_PAUSED;
|
||||
}
|
||||
}
|
||||
|
||||
fun setLoading(isLoading: Boolean) {
|
||||
if (isLoading) {
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader()
|
||||
} else {
|
||||
_loaderGame.visibility = View.GONE
|
||||
_loaderGame.stopAndResetLoader()
|
||||
}
|
||||
}
|
||||
|
||||
fun setLoading(expectedDurationMs: Int) {
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader(expectedDurationMs.toLong())
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import com.futo.platformplayer.formatDuration
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -154,10 +155,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
val onChapterClicked = Event1<IChapter>();
|
||||
|
||||
private val loaderOverlay: FrameLayout
|
||||
private val loaderIndeterminate: android.widget.ProgressBar
|
||||
private val loaderDeterminate: android.widget.ProgressBar
|
||||
private var determinateAnimator: ValueAnimator? = null
|
||||
private val _loaderGame: TargetTapLoaderView
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
|
||||
@@ -199,13 +197,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration);
|
||||
_control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause);
|
||||
|
||||
loaderOverlay = findViewById(R.id.loader_overlay)
|
||||
loaderIndeterminate = findViewById(R.id.loader_indeterminate)
|
||||
loaderDeterminate = findViewById(R.id.loader_determinate)
|
||||
|
||||
loaderOverlay.visibility = View.GONE
|
||||
loaderIndeterminate.visibility = View.GONE
|
||||
loaderDeterminate.visibility = View.GONE
|
||||
_loaderGame = findViewById(R.id.loader_overlay)
|
||||
_loaderGame.visibility = View.GONE
|
||||
|
||||
_control_chapter.setOnClickListener {
|
||||
_currentChapter?.let {
|
||||
@@ -884,37 +877,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
override fun setLoading(isLoading: Boolean) {
|
||||
determinateAnimator?.cancel()
|
||||
if (isLoading) {
|
||||
loaderOverlay.visibility = View.VISIBLE
|
||||
loaderIndeterminate.visibility = View.VISIBLE
|
||||
loaderDeterminate.visibility = View.GONE
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader()
|
||||
} else {
|
||||
loaderOverlay.visibility = View.GONE
|
||||
loaderIndeterminate.visibility = View.GONE
|
||||
loaderDeterminate.visibility = View.GONE
|
||||
_loaderGame.visibility = View.GONE
|
||||
_loaderGame.stopAndResetLoader()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setLoading(expectedDurationMs: Int) {
|
||||
determinateAnimator?.cancel()
|
||||
|
||||
loaderOverlay.visibility = View.VISIBLE
|
||||
loaderIndeterminate.visibility = View.GONE
|
||||
loaderDeterminate.visibility = View.VISIBLE
|
||||
loaderDeterminate.max = expectedDurationMs
|
||||
loaderDeterminate.progress = 0
|
||||
|
||||
determinateAnimator = ValueAnimator.ofInt(0, expectedDurationMs).apply {
|
||||
duration = expectedDurationMs.toLong()
|
||||
addUpdateListener { anim ->
|
||||
loaderDeterminate.progress = anim.animatedValue as Int
|
||||
if(loaderDeterminate.progress > loaderDeterminate.max - 10) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
}
|
||||
start()
|
||||
};
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader(expectedDurationMs.toLong())
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package com.futo.platformplayer.views.video
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
@@ -29,6 +31,8 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.exoplayer.source.SingleSampleMediaSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
@@ -52,9 +56,14 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -71,6 +80,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
@@ -117,7 +127,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
private var _didCallSourceChange = false;
|
||||
private var _lastState: Int = -1;
|
||||
|
||||
private val _swapIdAudio = AtomicInteger(0)
|
||||
private val _swapIdVideo = AtomicInteger(0)
|
||||
|
||||
var targetTrackVideoHeight = -1
|
||||
private set
|
||||
@@ -436,13 +447,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
|
||||
private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean {
|
||||
setLoading(false)
|
||||
val swapId = _swapIdVideo.incrementAndGet()
|
||||
_lastGeneratedDash = null;
|
||||
val didSet = when(videoSource) {
|
||||
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
|
||||
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
|
||||
is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true }
|
||||
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
|
||||
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume);
|
||||
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume, swapId);
|
||||
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
|
||||
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
|
||||
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
||||
@@ -453,11 +466,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
return didSet;
|
||||
}
|
||||
private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean {
|
||||
setLoading(false)
|
||||
val swapId = _swapIdAudio.incrementAndGet()
|
||||
val didSet = when(audioSource) {
|
||||
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
|
||||
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; }
|
||||
is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; }
|
||||
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume);
|
||||
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume, swapId);
|
||||
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
|
||||
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
|
||||
null -> { _lastAudioMediaSource = null; true; }
|
||||
@@ -564,7 +579,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean {
|
||||
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean, swapId: Int): Boolean {
|
||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||
|
||||
if(videoSource.hasGenerate) {
|
||||
@@ -583,6 +598,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
}
|
||||
val generated = generatedDef.await();
|
||||
if (_swapIdVideo.get() != swapId) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -708,7 +727,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean {
|
||||
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean, swapId: Int): Boolean {
|
||||
Logger.i(TAG, "Loading AudioSource [DashRaw]");
|
||||
if(audioSource.hasGenerate) {
|
||||
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
|
||||
@@ -726,6 +745,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
}
|
||||
val generated = generatedDef.await();
|
||||
if (_swapIdAudio.get() != swapId) {
|
||||
return@launch
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -889,6 +911,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
fun clear() {
|
||||
exoPlayer?.player?.stop();
|
||||
exoPlayer?.player?.clearMediaItems();
|
||||
setLoading(false)
|
||||
_swapIdVideo.incrementAndGet()
|
||||
_swapIdAudio.incrementAndGet()
|
||||
_lastVideoMediaSource = null;
|
||||
_lastAudioMediaSource = null;
|
||||
_lastSubtitleMediaSource = null;
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="@color/black">
|
||||
|
||||
<com.futo.platformplayer.views.others.CircularProgressBar
|
||||
<com.futo.platformplayer.views.TargetTapLoaderView
|
||||
android:id="@+id/test_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
app:progress="0%"
|
||||
app:strokeWidth="20dp" />
|
||||
android:layout_height="240dp" />
|
||||
</FrameLayout>
|
||||
@@ -65,36 +65,10 @@
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
<com.futo.platformplayer.views.TargetTapLoaderView
|
||||
android:id="@+id/loader_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:background="@color/black"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loader_indeterminate"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loader_determinate"
|
||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
||||
android:progressDrawable="@drawable/progress_bar"
|
||||
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>
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -189,4 +189,14 @@
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<com.futo.platformplayer.views.TargetTapLoaderView
|
||||
android:id="@+id/loader_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
Submodule app/src/stable/assets/sources/dailymotion updated: d115430011...850eb8122d
Submodule app/src/stable/assets/sources/odysee updated: d37268abe2...736c6b953a
Submodule app/src/stable/assets/sources/twitch updated: f526ce1b76...8de3ab18f5
Submodule app/src/stable/assets/sources/youtube updated: 98255d1773...2b724f21a7
Submodule app/src/unstable/assets/sources/dailymotion updated: d115430011...850eb8122d
Submodule app/src/unstable/assets/sources/odysee updated: d37268abe2...736c6b953a
Submodule app/src/unstable/assets/sources/twitch updated: f526ce1b76...8de3ab18f5
Submodule app/src/unstable/assets/sources/youtube updated: 98255d1773...2b724f21a7
Reference in New Issue
Block a user