Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2025-07-07 14:14:51 +02:00
commit 56c0f7bfaf
5 changed files with 123 additions and 18 deletions

View file

@ -64,6 +64,7 @@ import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Collections import java.util.Collections
import java.util.UUID import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
class StateCasting { class StateCasting {
private val _scopeIO = CoroutineScope(Dispatchers.IO); private val _scopeIO = CoroutineScope(Dispatchers.IO);
@ -89,6 +90,7 @@ class StateCasting {
var _resumeCastingDevice: CastingDeviceInfo? = null; var _resumeCastingDevice: CastingDeviceInfo? = null;
private var _nsdManager: NsdManager? = null private var _nsdManager: NsdManager? = null
val isCasting: Boolean get() = activeDevice != null; val isCasting: Boolean get() = activeDevice != null;
private val _castId = AtomicInteger(0)
private val _discoveryListeners = mapOf( private val _discoveryListeners = mapOf(
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), "_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
@ -432,13 +434,18 @@ class StateCasting {
action(); action();
} }
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean { fun cancel() {
_castId.incrementAndGet()
}
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 {
val ad = activeDevice ?: return false; val ad = activeDevice ?: return false;
if (ad.connectionState != CastConnectionState.CONNECTED) { if (ad.connectionState != CastConnectionState.CONNECTED) {
return false; return false;
} }
val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0;
val castId = _castId.incrementAndGet()
var sourceCount = 0; var sourceCount = 0;
if (videoSource != null) sourceCount++; if (videoSource != null) sourceCount++;
@ -466,7 +473,7 @@ class StateCasting {
Logger.i(TAG, "Casting as raw DASH"); Logger.i(TAG, "Casting as raw DASH");
try { try {
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed); castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e); Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
} }
@ -529,7 +536,7 @@ class StateCasting {
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
try { try {
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed); castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e); Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
} }
@ -539,7 +546,7 @@ class StateCasting {
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
try { try {
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed); castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e); Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
} }
@ -1236,7 +1243,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 +1290,48 @@ class StateCasting {
} }
} }
var dashContent = withContext(Dispatchers.IO) { var dashContent: String = withContext(Dispatchers.IO) {
stopVideo()
//TODO: Include subtitlesURl in the future //TODO: Include subtitlesURl in the future
return@withContext if (audioSource != null && videoSource != null) { val deferred = if (audioSource != null && videoSource != null) {
JSDashManifestMergingRawSource(videoSource, audioSource).generate() JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO)
} else if (audioSource != null) { } else if (audioSource != null) {
audioSource.generate() audioSource.generateAsync(_scopeIO)
} else if (videoSource != null) { } else if (videoSource != null) {
videoSource.generate() videoSource.generateAsync(_scopeIO)
} else { } else {
Logger.e(TAG, "Expected at least audio or video to be set") Logger.e(TAG, "Expected at least audio or video to be set")
null null
} }
if (deferred != null) {
try {
withContext(Dispatchers.Main) {
if (deferred.estDuration >= 0) {
onLoadingEstimate?.invoke(deferred.estDuration)
} else {
onLoading?.invoke(true)
}
}
deferred.await()
} finally {
if (castId == _castId.get()) {
withContext(Dispatchers.Main) {
onLoading?.invoke(false)
}
}
}
} else {
return@withContext null
}
} ?: throw Exception("Dash is null") } ?: throw Exception("Dash is null")
if (castId != _castId.get()) {
Log.i(TAG, "Get DASH cancelled.")
return emptyList()
}
for (representation in representationRegex.findAll(dashContent)) { for (representation in representationRegex.findAll(dashContent)) {
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found") val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
dashContent = mediaInitializationRegex.replace(dashContent) { dashContent = mediaInitializationRegex.replace(dashContent) {

View file

@ -806,6 +806,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();
@ -1899,7 +1901,13 @@ 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)")
if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)) { val castSucceeded = StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
_cast.setLoading(it)
}, onLoadingEstimate = {
_cast.setLoading(it)
})
if (castSucceeded) {
_cast.setVideoDetails(video, resumePositionMs / 1000); _cast.setVideoDetails(video, resumePositionMs / 1000);
setCastEnabled(true); setCastEnabled(true);
} else throw IllegalStateException("Disconnected cast during loading"); } else throw IllegalStateException("Disconnected cast during loading");
@ -2553,8 +2561,7 @@ class VideoDetailView : ConstraintLayout {
_cast.visibility = View.VISIBLE; _cast.visibility = View.VISIBLE;
} else { } else {
StateCasting.instance.stopVideo(); StateCasting.instance.stopVideo();
_cast.stopTimeJob(); _cast.cancel()
_cast.visibility = View.GONE;
if (video?.isLive == false) { if (video?.isLive == false) {
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed()); _player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());

View file

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

View file

@ -3,10 +3,12 @@ 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.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
@ -29,6 +31,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
@ -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.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
@ -71,6 +80,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 {
@ -117,7 +127,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
@ -436,13 +447,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; }
@ -453,11 +466,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; }
@ -564,7 +579,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) {
@ -583,6 +598,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)
} }
@ -708,7 +727,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) {
@ -726,6 +745,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)
} }
@ -889,6 +911,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
fun clear() { fun clear() {
exoPlayer?.player?.stop(); exoPlayer?.player?.stop();
exoPlayer?.player?.clearMediaItems(); exoPlayer?.player?.clearMediaItems();
setLoading(false)
_swapIdVideo.incrementAndGet()
_swapIdAudio.incrementAndGet()
_lastVideoMediaSource = null; _lastVideoMediaSource = null;
_lastAudioMediaSource = null; _lastAudioMediaSource = null;
_lastSubtitleMediaSource = null; _lastSubtitleMediaSource = null;

View file

@ -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>