diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index 92f83598..c7bf3e5c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -18,6 +18,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import android.widget.LinearLayout import android.widget.ImageButton import android.widget.TextView import androidx.annotation.OptIn @@ -119,6 +120,15 @@ class FutoVideoPlayer : FutoVideoPlayerBase { private val _control_duration_fullscreen: TextView; private val _control_pause_fullscreen: ImageButton; + // LIVE pill: shown only when current media item is live; dot color reflects live-edge proximity. + private val _live_pill: LinearLayout + private val _live_pill_dot: View + private val _live_pill_fullscreen: LinearLayout + private val _live_pill_dot_fullscreen: View + private val _text_divider: TextView + private val _text_divider_fullscreen: TextView + private var _wasAtLiveEdge: Boolean = true + private val _title_fullscreen: TextView; private val _author_fullscreen: TextView; private var _shouldRestartHideJobOnPlaybackStateChange: Boolean = false; @@ -189,6 +199,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase { _buttonPrevious = videoControls.findViewById(R.id.button_previous); _control_time = videoControls.findViewById(R.id.text_position); _control_duration = videoControls.findViewById(R.id.text_duration); + _live_pill = videoControls.findViewById(R.id.live_pill_container) + _live_pill_dot = videoControls.findViewById(R.id.live_pill_dot) + _text_divider = videoControls.findViewById(R.id.text_divider) _videoControls_fullscreen = findViewById(R.id.video_player_controller_fullscreen); _control_autoplay_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_autoplay); @@ -206,6 +219,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase { _control_time_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_position); _control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration); _control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause); + _live_pill_fullscreen = _videoControls_fullscreen.findViewById(R.id.live_pill_container) + _live_pill_dot_fullscreen = _videoControls_fullscreen.findViewById(R.id.live_pill_dot) + _text_divider_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_divider) _loaderGame = findViewById(R.id.loader_overlay) _loaderGame.visibility = View.GONE @@ -475,6 +491,12 @@ class FutoVideoPlayer : FutoVideoPlayerBase { _time_bar_fullscreen.setBufferedPosition(bufferedPosition); _time_bar.setBufferedPosition(bufferedPosition); + // While live, refresh the LIVE pill's edge state so the dot reflects whether the user + // is at the live edge or seeked behind. Cheap and only updates when state actually changes. + if (isLive) { + updateLiveEdgeState() + } + onTimeBarChanged.emit(position, bufferedPosition); if(!_currentChapterLoopActive) @@ -501,6 +523,23 @@ class FutoVideoPlayer : FutoVideoPlayerBase { } } + // Toggle LIVE pill / time UI when the underlying media item changes liveness. + // The base class emits this on Timeline / MediaItem transitions. + onLiveChanged.subscribe { live -> + CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) { + applyLiveUI(live) + } + } + + val jumpToLiveListener = View.OnClickListener { + seekToLiveEdge() + } + _live_pill.setOnClickListener(jumpToLiveListener) + _live_pill_fullscreen.setOnClickListener(jumpToLiveListener) + + // Apply once at construction in case we attach to an already-live media item. + applyLiveUI(isLive) + updateLoopVideoUI(); if(!isInEditMode) { @@ -897,6 +936,58 @@ class FutoVideoPlayer : FutoVideoPlayerBase { } } + /** + * Applies (or reverts) live-stream-specific control affordances: + * - shows/hides the LIVE pill + * - hides the duration text + divider when live (duration shown by the pill) + * - hides the loop button (looping a live stream is meaningless) + * - hides the chapter text (live streams from the source plugins do not provide chapters) + * + * Position text is kept visible because for HLS DVR streams it shows offset within the + * available seek window, which is useful information. + */ + private fun applyLiveUI(live: Boolean) { + val pillVis = if (live) View.VISIBLE else View.GONE + val timeVis = if (live) View.GONE else View.VISIBLE + _live_pill.visibility = pillVis + _live_pill_fullscreen.visibility = pillVis + _text_divider.visibility = timeVis + _text_divider_fullscreen.visibility = timeVis + _control_duration.visibility = timeVis + _control_duration_fullscreen.visibility = timeVis + + if (live) { + // Loop / chapter UI is meaningless on live; hide and reset. + _control_loop.visibility = View.GONE + _control_loop_fullscreen.visibility = View.GONE + _control_chapter.visibility = View.GONE + _control_chapter_fullscreen.visibility = View.GONE + updateLiveEdgeState() + } else { + _control_loop.visibility = View.VISIBLE + _control_loop_fullscreen.visibility = View.VISIBLE + _control_chapter.visibility = View.VISIBLE + _control_chapter_fullscreen.visibility = View.VISIBLE + } + } + + /** + * Updates the LIVE pill's dot + background to reflect whether playback is at the live edge. + * Idempotent: only mutates when state changes to avoid invalidations on every progress tick. + */ + private fun updateLiveEdgeState() { + val atEdge = isAtLiveEdge + if (atEdge == _wasAtLiveEdge) return + _wasAtLiveEdge = atEdge + Logger.i(TAG, "LIVE pill -> ${if (atEdge) "AT EDGE" else "BEHIND"} (offset=${liveOffsetMs}ms target=${targetLiveOffsetMs}ms)") + val bg = if (atEdge) R.drawable.background_live_pill else R.drawable.background_live_pill_behind + val dot = if (atEdge) R.drawable.dot_live_edge else R.drawable.dot_live_behind + _live_pill.setBackgroundResource(bg) + _live_pill_fullscreen.setBackgroundResource(bg) + _live_pill_dot.setBackgroundResource(dot) + _live_pill_dot_fullscreen.setBackgroundResource(dot) + } + fun setGestureSoundFactor(soundFactor: Float) { gestureControl.setSoundFactor(soundFactor); } diff --git a/app/src/main/res/drawable/background_live_pill.xml b/app/src/main/res/drawable/background_live_pill.xml new file mode 100644 index 00000000..e0cc9fb4 --- /dev/null +++ b/app/src/main/res/drawable/background_live_pill.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/background_live_pill_behind.xml b/app/src/main/res/drawable/background_live_pill_behind.xml new file mode 100644 index 00000000..79fb7d70 --- /dev/null +++ b/app/src/main/res/drawable/background_live_pill_behind.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/dot_live_behind.xml b/app/src/main/res/drawable/dot_live_behind.xml new file mode 100644 index 00000000..a99d059a --- /dev/null +++ b/app/src/main/res/drawable/dot_live_behind.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/dot_live_edge.xml b/app/src/main/res/drawable/dot_live_edge.xml new file mode 100644 index 00000000..01d8e7cb --- /dev/null +++ b/app/src/main/res/drawable/dot_live_edge.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/video_player_ui.xml b/app/src/main/res/layout/video_player_ui.xml index 8e31747c..0015512b 100644 --- a/app/src/main/res/layout/video_player_ui.xml +++ b/app/src/main/res/layout/video_player_ui.xml @@ -195,6 +195,47 @@ app:layout_constraintTop_toTopOf="@id/text_position" app:layout_constraintBottom_toBottomOf="@id/text_position"/> + + + + + + + + + + + + + + + + Stop Scan QR code Help + Jump to live edge Change Polycentric profile picture Recommendations