From feeba10429eb85d11f25fe41660e32cbded6e23b Mon Sep 17 00:00:00 2001 From: Marcus Hanestad Date: Thu, 4 Sep 2025 13:41:11 +0200 Subject: [PATCH] casting: undo formatting to VideoDetailView --- .../mainactivity/main/VideoDetailView.kt | 175 +++++++++--------- 1 file changed, 87 insertions(+), 88 deletions(-) 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 04bf2f42..329c4e12 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 @@ -576,7 +576,9 @@ class VideoDetailView : ConstraintLayout { if(chapter?.type == ChapterType.SKIPPABLE) { _layoutSkip.visibility = VISIBLE; } else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) { - if (!StateCasting.instance.videoSeekTo(chapter.timeEnd)) { + if (StateCasting.instance.activeDevice != null) { + StateCasting.instance.videoSeekTo(chapter.timeEnd) + } else { _player.seekTo((chapter.timeEnd * 1000).toLong()); } @@ -680,7 +682,9 @@ class VideoDetailView : ConstraintLayout { } else -> {} } - } + }; + + updatePillButtonVisibilities(); StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) { val activeDevice = StateCasting.instance.activeDevice; @@ -704,8 +708,6 @@ class VideoDetailView : ConstraintLayout { } }; - updatePillButtonVisibilities(); - _cast.onTimeJobTimeChanged_s.subscribe { if (_isCasting) { setLastPositionMilliseconds((it * 1000.0).toLong(), true); @@ -883,7 +885,7 @@ class VideoDetailView : ConstraintLayout { if (ad != null) { val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong()); if(currentChapter?.type == ChapterType.SKIPPABLE) { - ad.seekTo(currentChapter.timeEnd); + StateCasting.instance.videoSeekTo(currentChapter.timeEnd); } } else { val currentChapter = _player.getCurrentChapter(_player.position); @@ -985,11 +987,11 @@ class VideoDetailView : ConstraintLayout { } }, _chapters?.let { - if(it != null && it.size > 0) - RoundButton(context, R.drawable.ic_list, "Chapters", TAG_CHAPTERS) { - showChaptersUI(); - } - else null + if(it != null && it.size > 0) + RoundButton(context, R.drawable.ic_list, "Chapters", TAG_CHAPTERS) { + showChaptersUI(); + } + else null }, if(video?.isLive ?: false) RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) { @@ -1002,6 +1004,17 @@ class VideoDetailView : ConstraintLayout { } } _slideUpOverlay?.hide(); + } else if(video is JSVideoDetails && (video as JSVideoDetails).hasVODEvents()) + RoundButton(context, R.drawable.ic_chat, context.getString(R.string.vod_chat), TAG_VODCHAT) { + video?.let { + try { + loadVODChat(it); + } + catch(ex: Throwable) { + Logger.e(TAG, "Failed to reopen vod chat", ex); + } + } + _slideUpOverlay?.hide(); } else null, if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (isAudioOnlyUserAction) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) { if (!isAudioOnlyUserAction) { @@ -1023,14 +1036,14 @@ class VideoDetailView : ConstraintLayout { }; } else null, - RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) { - video?.let { - Logger.i(TAG, "Share preventPictureInPicture = true"); - preventPictureInPicture = true; - shareVideo(); - }; - _slideUpOverlay?.hide(); - }, + RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) { + video?.let { + Logger.i(TAG, "Share preventPictureInPicture = true"); + preventPictureInPicture = true; + shareVideo(); + }; + _slideUpOverlay?.hide(); + }, if(!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) { this.startPictureInPicture(); @@ -1126,19 +1139,23 @@ class VideoDetailView : ConstraintLayout { //Lifecycle + var isLoginStop = false; //TODO: This is a bit jank, but easiest solution for now without reworking flow. (Alternatively, fix MainActivity getting stopped/disposing video) fun onResume() { Logger.v(TAG, "onResume"); _onPauseCalled = false; + val wasLoginCall = isLoginStop; + isLoginStop = false; + Logger.i(TAG, "_video: ${video?.name ?: "no video"}"); Logger.i(TAG, "_didStop: $_didStop"); //Recover cancelled loads if(video == null) { val t = (lastPositionMilliseconds / 1000.0f).roundToLong(); - if(_searchVideo != null) + if(_searchVideo != null && !wasLoginCall) setVideoOverview(_searchVideo!!, true, t); - else if(_url != null) + else if(_url != null && !wasLoginCall) setVideo(_url!!, t, _playWhenReady); } else if(_didStop) { @@ -1155,6 +1172,9 @@ class VideoDetailView : ConstraintLayout { isAudioOnlyUserAction = false; _buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.background); } + else { + _buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.video); + } } if(!_player.isFitMode && !_player.isFullScreen && !fragment.isInPictureInPicture) _player.fitHeight(); @@ -1167,7 +1187,7 @@ class VideoDetailView : ConstraintLayout { _onPauseCalled = true; _taskLoadVideo.cancel(); - if (StateCasting.instance.isCasting) + if(StateCasting.instance.isCasting) return; if(isAudioOnlyUserAction) @@ -1200,6 +1220,7 @@ class VideoDetailView : ConstraintLayout { _taskLoadVideo.cancel(); handleStop(); _didStop = true; + onShouldEnterPictureInPictureChanged.emit() Logger.i(TAG, "_didStop set to true"); StatePlayer.instance.rotationLock = false; @@ -1952,7 +1973,8 @@ class VideoDetailView : ConstraintLayout { return; } - if (!StateCasting.instance.isCasting) { + val isCasting = StateCasting.instance.isCasting + if (!isCasting) { setCastEnabled(false); val isLimitedVersion = StatePlatform.instance.getContentClientOrNull(video.url)?.let { @@ -2023,8 +2045,8 @@ class VideoDetailView : ConstraintLayout { private suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { try { val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin() - else if (audioSource is JSSource) audioSource.getUnderlyingPlugin() - else null + else if (audioSource is JSSource) audioSource.getUnderlyingPlugin() + else null val startId = plugin?.getUnderlyingPlugin()?.runtimeId try { @@ -2227,9 +2249,7 @@ class VideoDetailView : ConstraintLayout { } } - val currentPlaybackRate = (if (_isCasting) { - StateCasting.instance.activeDevice?.speed - } else _player.getPlaybackRate()) ?: 1.0 + val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0 _overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let { (it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString()) }; @@ -2347,15 +2367,11 @@ class VideoDetailView : ConstraintLayout { ?.distinct() ?.toList() ?: listOf() else audioSources?.toList() ?: listOf(); - val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() ?: false - val currentPlaybackRate = if (_isCasting) { - StateCasting.instance.activeDevice?.speed - } else { - _player.getPlaybackRate() - } + val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() == true + val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate() val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null; _overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString( - R.string.quality), null, true, + R.string.quality), null, true, qualityPlaybackSpeedTitle, if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply { val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds(); @@ -2366,9 +2382,7 @@ class VideoDetailView : ConstraintLayout { setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate)); onClick.subscribe { v -> - val currentPlaybackSpeed = if (_isCasting) { - StateCasting.instance.activeDevice?.speed - } else _player.getPlaybackRate(); + val currentPlaybackSpeed = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate(); var playbackSpeedString = v; val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep(); if(v == "+") @@ -2376,9 +2390,14 @@ class VideoDetailView : ConstraintLayout { else if(v == "-") playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString(); val newPlaybackSpeed = playbackSpeedString.toDouble(); - if (_isCasting && StateCasting.instance.activeDevice?.canSetSpeed() ?: false) { + if (_isCasting) { + val ad = StateCasting.instance.activeDevice ?: return@subscribe + if (!ad.canSetSpeed()) { + return@subscribe + } + qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); - StateCasting.instance.changeSpeed(newPlaybackSpeed) + ad.changeSpeed(newPlaybackSpeed) setSelected(playbackSpeedString); } else { qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); @@ -2495,8 +2514,9 @@ class VideoDetailView : ConstraintLayout { private fun handlePlay() { Logger.i(TAG, "handlePlay") if (!StateCasting.instance.resumeVideo()) { - _player.play() + _player.play(); } + onShouldEnterPictureInPictureChanged.emit() //TODO: This was needed because handleLowerVolume was done. //_player.setVolume(1.0f); @@ -2511,26 +2531,28 @@ class VideoDetailView : ConstraintLayout { private fun handlePause() { Logger.i(TAG, "handlePause") if (!StateCasting.instance.pauseVideo()) { - _player.pause() + _player.pause(); } + onShouldEnterPictureInPictureChanged.emit() } private fun handleSeek(ms: Long) { Logger.i(TAG, "handleSeek(ms=$ms)") if (!StateCasting.instance.videoSeekTo(ms.toDouble() / 1000.0)) { - _player.seekTo(ms) + _player.seekTo(ms); } } private fun handleStop() { Logger.i(TAG, "handleStop") if (!StateCasting.instance.stopVideo()) { - _player.stop() + _player.stop(); } } private fun handlePlayChanged(playing: Boolean) { Logger.i(TAG, "handlePlayChanged(playing=$playing)") - if (StateCasting.instance.isCasting) { + val ad = StateCasting.instance.activeDevice; + if (ad != null) { _cast.setIsPlaying(playing); } else { StatePlayer.instance.updateMediaSession( null); @@ -2572,21 +2594,11 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.Main) { try { - if (StateCasting.instance.activeDevice != null) { - val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0 - val speed = StateCasting.instance.activeDevice?.speed ?: 1.0 - castIfAvailable( - context.contentResolver, - video, - videoSource, - _lastAudioSource, - _lastSubtitleSource, - (expectedCurrentTime * 1000.0).toLong(), - speed - ) - } else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true)) { + val d = StateCasting.instance.activeDevice; + if (d != null && d.connectionState == CastConnectionState.CONNECTED) + 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? - } } catch (e: Throwable) { Logger.e(TAG, "handleSelectVideoTrack failed", e) } @@ -2603,21 +2615,11 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.Main) { try { - if (StateCasting.instance.activeDevice != null) { - val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0 - val speed = StateCasting.instance.activeDevice?.speed ?: 1.0 - castIfAvailable( - context.contentResolver, - video, - _lastVideoSource, - audioSource, - _lastSubtitleSource, - (expectedCurrentTime * 1000.0).toLong(), - speed - ) - } else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) { + val d = StateCasting.instance.activeDevice; + if (d != null && d.connectionState == CastConnectionState.CONNECTED) + castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed) + else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) _player.hideControls(false); //TODO: Disable player? - } } catch (e: Throwable) { Logger.e(TAG, "handleSelectAudioTrack failed", e) } @@ -2635,19 +2637,10 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.Main) { try { - if (StateCasting.instance.activeDevice != null) { - val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0 - val speed = StateCasting.instance.activeDevice?.speed ?: 1.0 - castIfAvailable( - context.contentResolver, - video, - _lastVideoSource, - _lastAudioSource, - toSet, - (expectedCurrentTime * 1000.0).toLong(), - speed - ) - } else { + val d = StateCasting.instance.activeDevice; + if (d != null && d.connectionState == CastConnectionState.CONNECTED) + castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); + else { _player.swapSubtitles(toSet); } } catch (e: Throwable) { @@ -3287,8 +3280,13 @@ class VideoDetailView : ConstraintLayout { val id = e.config.let { if(it is SourcePluginConfig) it.id else null }; val didLogin = if(id == null) false - else StatePlugins.instance.loginPlugin(context, id) { - fetchVideo(); + else { + isLoginStop = true; + StatePlugins.instance.loginPlugin(context, id) { + fragment.lifecycleScope.launch(Dispatchers.Main) { + fetchVideo(); + } + } } if(!didLogin) UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login"); @@ -3313,7 +3311,7 @@ class VideoDetailView : ConstraintLayout { UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo, null, fragment); } else { StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString( - R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION) + R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION) } } .exception { @@ -3466,6 +3464,7 @@ class VideoDetailView : ConstraintLayout { const val TAG_SHARE = "share"; const val TAG_OVERLAY = "overlay"; const val TAG_LIVECHAT = "livechat"; + const val TAG_VODCHAT = "vodchat"; const val TAG_CHAPTERS = "chapters"; const val TAG_OPEN = "open"; const val TAG_SEND_TO_DEVICE = "send_to_device";