Fixed casting.

This commit is contained in:
Koen J 2025-07-07 12:45:45 +02:00
commit 37dc778009
4 changed files with 93 additions and 13 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

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