diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 1e8e1830..a4084383 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -64,6 +64,7 @@ import java.net.URLDecoder import java.net.URLEncoder import java.util.Collections import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger class StateCasting { private val _scopeIO = CoroutineScope(Dispatchers.IO); @@ -89,6 +90,7 @@ class StateCasting { var _resumeCastingDevice: CastingDeviceInfo? = null; private var _nsdManager: NsdManager? = null val isCasting: Boolean get() = activeDevice != null; + private val _castId = AtomicInteger(0) private val _discoveryListeners = mapOf( "_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), @@ -432,13 +434,18 @@ class StateCasting { 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; if (ad.connectionState != CastConnectionState.CONNECTED) { return false; } val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; + val castId = _castId.incrementAndGet() var sourceCount = 0; if (videoSource != null) sourceCount++; @@ -466,7 +473,7 @@ class StateCasting { Logger.i(TAG, "Casting as raw DASH"); 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) { 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) { 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) { Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e); } @@ -539,7 +546,7 @@ class StateCasting { StateApp.instance.scope.launch(Dispatchers.IO) { 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) { Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e); } @@ -1236,7 +1243,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 { + 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 { val ad = activeDevice ?: return listOf(); 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 - 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) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index e9be9fd6..d7ca2f7d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -806,6 +806,8 @@ class VideoDetailView : ConstraintLayout { _lastVideoSource = null; _lastAudioSource = null; _lastSubtitleSource = null; + _cast.cancel() + StateCasting.instance.cancel() video = null; _container_content_liveChat?.close(); _player.clear(); @@ -1899,7 +1901,13 @@ 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)") - 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); setCastEnabled(true); } else throw IllegalStateException("Disconnected cast during loading"); @@ -2553,8 +2561,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()); diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt index fe941fea..161f3dc3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt @@ -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()) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/view_cast.xml b/app/src/main/res/layout/view_cast.xml index 7c82c130..7b0779cd 100644 --- a/app/src/main/res/layout/view_cast.xml +++ b/app/src/main/res/layout/view_cast.xml @@ -189,4 +189,14 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> + + \ No newline at end of file