Skip to content

Commit 1a353ab

Browse files
committed
Refactor VideoPlayerUi to improve accessibility for Java subclasses and enhance chapter marker handling
1 parent 2ed12aa commit 1a353ab

File tree

1 file changed

+62
-36
lines changed

1 file changed

+62
-36
lines changed

app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.kt

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import android.view.View
2020
import android.widget.LinearLayout
2121
import android.widget.RelativeLayout
2222
import android.widget.SeekBar
23-
import androidx.annotation.OptIn
2423
import androidx.appcompat.content.res.AppCompatResources
2524
import androidx.appcompat.view.ContextThemeWrapper
2625
import androidx.appcompat.widget.AppCompatImageButton
@@ -101,7 +100,7 @@ abstract class VideoPlayerUi protected constructor(
101100
// region Views
102101

103102
@JvmField
104-
protected var binding: PlayerBinding = playerBinding
103+
var binding: PlayerBinding = playerBinding
105104

106105
private val controlsVisibilityHandler = Handler(Looper.getMainLooper())
107106

@@ -113,7 +112,7 @@ abstract class VideoPlayerUi protected constructor(
113112
// Popup menus ("popup" means that they pop up, not that they belong to the popup player)
114113

115114
@JvmField
116-
protected var isSomePopupMenuVisible = false
115+
var isSomePopupMenuVisible = false
117116

118117
private var qualityPopupMenu: PopupMenu? = null
119118
private var audioTrackPopupMenu: PopupMenu? = null
@@ -125,7 +124,7 @@ abstract class VideoPlayerUi protected constructor(
125124

126125
// Gestures
127126

128-
private var gestureDetector: GestureDetector? = null
127+
internal var gestureDetector: GestureDetector? = null
129128
private var playerGestureListener: BasePlayerGestureListener? = null
130129
private var onLayoutChangeListener: View.OnLayoutChangeListener? = null
131130

@@ -177,7 +176,11 @@ abstract class VideoPlayerUi protected constructor(
177176
binding.itemsList.isNestedScrollingEnabled = false
178177
}
179178

180-
internal abstract fun buildGestureListener(): BasePlayerGestureListener
179+
// Must be `protected` (not `internal`) so that the Java subclasses MainPlayerUi and
180+
// PopupPlayerUi can override it. Kotlin `internal` compiles to a JVM-mangled name
181+
// (e.g. `buildGestureListener$app_debug`) that Java cannot reference, which causes a
182+
// "must be declared abstract or implement abstract method" compile error in those classes.
183+
protected abstract fun buildGestureListener(): BasePlayerGestureListener
181184

182185
protected open fun initListeners() {
183186
binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked))
@@ -192,7 +195,7 @@ abstract class VideoPlayerUi protected constructor(
192195
binding.playbackLiveSync.setOnClickListener(makeOnClickListener(player::seekToDefault))
193196

194197
playerGestureListener = buildGestureListener()
195-
gestureDetector = GestureDetector(context, playerGestureListener)
198+
gestureDetector = GestureDetector(context, playerGestureListener!!)
196199
binding.root.setOnTouchListener(playerGestureListener)
197200

198201
binding.repeatButton.setOnClickListener { onRepeatClicked() }
@@ -392,6 +395,14 @@ abstract class VideoPlayerUi protected constructor(
392395

393396
// #6825 - Ensure that the shuffle-button is in the correct state on the UI
394397
setShuffleButton(player.exoPlayer.shuffleModeEnabled)
398+
399+
// Seed chapter markers in case onMetadataChanged already fired before this UI was
400+
// attached to the player (e.g. when switching player types or restoring a session).
401+
player.currentStreamInfo.ifPresent { info ->
402+
currentChapters = info.streamSegments ?: emptyList()
403+
lastChapterForHaptic = null
404+
(binding.playbackSeekBar as ChaptersSeekBar).setChapters(currentChapters, info.duration)
405+
}
395406
}
396407

397408
abstract fun removeViewFromParent()
@@ -497,18 +508,33 @@ abstract class VideoPlayerUi protected constructor(
497508
if (duration != binding.playbackSeekBar.max) {
498509
setVideoDurationToControls(duration)
499510
}
511+
512+
// If chapter metadata callback was missed, recover from currentStreamInfo.
513+
if (currentChapters.isEmpty()) {
514+
player.currentStreamInfo.ifPresent { info ->
515+
val streamSegments = info.streamSegments ?: emptyList()
516+
if (streamSegments.isNotEmpty()) {
517+
currentChapters = streamSegments
518+
lastChapterForHaptic = null
519+
(binding.playbackSeekBar as? ChaptersSeekBar)
520+
?.setChapters(currentChapters, info.duration)
521+
}
522+
}
523+
}
524+
500525
if (player.currentState != STATE_PAUSED) {
501526
updatePlayBackElementsCurrentDuration(currentProgress)
502527
}
503528
if (player.isLoading || bufferPercent > 90) {
504529
binding.playbackSeekBar.secondaryProgress =
505530
(binding.playbackSeekBar.max * (bufferPercent.toFloat() / 100)).toInt()
506531
}
532+
507533
if (DEBUG && bufferPercent % 20 == 0) { // Limit log
508534
Log.d(
509535
TAG,
510536
"notifyProgressUpdateToListeners() called with: " +
511-
"isVisible = ${isControlsVisible()}, " +
537+
"isVisible = $isControlsVisible, " +
512538
"currentProgress = [$currentProgress], " +
513539
"duration = [$duration], bufferPercent = [$bufferPercent]"
514540
)
@@ -526,7 +552,7 @@ abstract class VideoPlayerUi protected constructor(
526552
if (player.currentState != STATE_PAUSED_SEEK) {
527553
binding.playbackSeekBar.progress = currentProgress
528554
}
529-
binding.playbackCurrentTime.text = getTimeString(currentProgress)
555+
binding.playbackCurrentTime.text = getTimeString(currentProgress.toLong())
530556
}
531557

532558
/**
@@ -535,7 +561,7 @@ abstract class VideoPlayerUi protected constructor(
535561
* @param duration the video duration, in milliseconds
536562
*/
537563
private fun setVideoDurationToControls(duration: Int) {
538-
binding.playbackEndTime.text = getTimeString(duration)
564+
binding.playbackEndTime.text = getTimeString(duration.toLong())
539565

540566
binding.playbackSeekBar.max = duration
541567
// This is important for Android TVs otherwise it would apply the default from
@@ -558,7 +584,7 @@ abstract class VideoPlayerUi protected constructor(
558584
)
559585
}
560586

561-
binding.currentDisplaySeek.text = getTimeString(progress)
587+
binding.currentDisplaySeek.text = getTimeString(progress.toLong())
562588

563589
// Seekbar Preview Thumbnail
564590
SeekbarPreviewThumbnailHelper
@@ -645,7 +671,7 @@ abstract class VideoPlayerUi protected constructor(
645671
player.exoPlayer.play()
646672
}
647673

648-
binding.playbackCurrentTime.text = getTimeString(seekBar.progress)
674+
binding.playbackCurrentTime.text = getTimeString(seekBar.progress.toLong())
649675
binding.currentDisplaySeek.animate(false, 200, AnimationType.SCALE_AND_ALPHA)
650676
binding.currentSeekbarPreviewThumbnail.animate(false, 200, AnimationType.SCALE_AND_ALPHA)
651677
binding.currentChapterTitle.animate(false, 200, AnimationType.SCALE_AND_ALPHA)
@@ -681,7 +707,8 @@ abstract class VideoPlayerUi protected constructor(
681707

682708
// region Controls showing / hiding
683709

684-
fun isControlsVisible(): Boolean = binding.playbackControlRoot.visibility == View.VISIBLE
710+
val isControlsVisible: Boolean
711+
get() = binding.playbackControlRoot.visibility == View.VISIBLE
685712

686713
fun showControlsThenHide() {
687714
if (DEBUG) {
@@ -772,10 +799,9 @@ abstract class VideoPlayerUi protected constructor(
772799
return false
773800
}
774801

775-
open fun isFullscreen(): Boolean {
802+
open val isFullscreen: Boolean
776803
// only MainPlayerUi can be in fullscreen, so overridden there
777-
return false
778-
}
804+
get() = false
779805

780806
/**
781807
* Update the play/pause button to reflect the action that will be performed when clicked.
@@ -807,7 +833,7 @@ abstract class VideoPlayerUi protected constructor(
807833
override fun onPrepared() {
808834
super.onPrepared()
809835
setVideoDurationToControls(player.exoPlayer.duration.toInt())
810-
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed)
836+
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed.toDouble())
811837
}
812838

813839
override fun onBlocked() {
@@ -865,7 +891,7 @@ abstract class VideoPlayerUi protected constructor(
865891

866892
// Don't let UI elements popup during double tap seeking. This state is entered sometimes
867893
// during seeking/loading. This if-else check ensures that the controls aren't popping up.
868-
if (!playerGestureListener!!.isDoubleTapping()) {
894+
if (!playerGestureListener!!.isDoubleTapping) {
869895
showControls(400)
870896
binding.loadingPanel.visibility = View.GONE
871897

@@ -982,7 +1008,7 @@ abstract class VideoPlayerUi protected constructor(
9821008

9831009
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
9841010
super.onPlaybackParametersChanged(playbackParameters)
985-
binding.playbackSpeed.text = formatSpeed(playbackParameters.speed)
1011+
binding.playbackSpeed.text = formatSpeed(playbackParameters.speed.toDouble())
9861012
}
9871013

9881014
override fun onRenderedFirstFrame() {
@@ -1005,10 +1031,13 @@ abstract class VideoPlayerUi protected constructor(
10051031
seekbarPreviewThumbnailHolder.resetFrom(player.context, info.previewFrames)
10061032

10071033
// Chapter markers on seekbar
1008-
currentChapters = info.streamSegments ?: emptyList()
1034+
val rawSegments = info.streamSegments
1035+
currentChapters = rawSegments ?: emptyList()
10091036
lastChapterForHaptic = null
1010-
(binding.playbackSeekBar as ChaptersSeekBar)
1011-
.setChapters(currentChapters, info.duration)
1037+
val seekBar = binding.playbackSeekBar
1038+
if (seekBar is ChaptersSeekBar) {
1039+
seekBar.setChapters(currentChapters, info.duration)
1040+
}
10121041
binding.currentChapterTitle.visibility = View.GONE
10131042
}
10141043

@@ -1134,10 +1163,10 @@ abstract class VideoPlayerUi protected constructor(
11341163
POPUP_MENU_ID_PLAYBACK_SPEED,
11351164
i,
11361165
Menu.NONE,
1137-
formatSpeed(PLAYBACK_SPEEDS[i])
1166+
formatSpeed(PLAYBACK_SPEEDS[i].toDouble())
11381167
)
11391168
}
1140-
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed)
1169+
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed.toDouble())
11411170
playbackSpeedPopupMenu!!.setOnMenuItemClickListener(this)
11421171
playbackSpeedPopupMenu!!.setOnDismissListener(this)
11431172
}
@@ -1287,7 +1316,7 @@ abstract class VideoPlayerUi protected constructor(
12871316
val speedIndex = menuItem.itemId
12881317
val speed = PLAYBACK_SPEEDS[speedIndex]
12891318
player.setPlaybackSpeed(speed)
1290-
binding.playbackSpeed.text = formatSpeed(speed)
1319+
binding.playbackSpeed.text = formatSpeed(speed.toDouble())
12911320
false
12921321
}
12931322

@@ -1360,7 +1389,6 @@ abstract class VideoPlayerUi protected constructor(
13601389
isSomePopupMenuVisible = true
13611390
}
13621391

1363-
fun isSomePopupMenuVisible(): Boolean = isSomePopupMenuVisible
13641392
// endregion
13651393

13661394
// region Captions (text tracks)
@@ -1376,11 +1404,11 @@ abstract class VideoPlayerUi protected constructor(
13761404
}
13771405

13781406
// Extract all loaded languages
1379-
val textTracks = currentTracks.groups.filter { it.type == C.TRACK_TYPE_TEXT }
1407+
val textTracks: List<Tracks.Group> = currentTracks.groups.filter { it.type == C.TRACK_TYPE_TEXT }
13801408
val availableLanguages = textTracks
1381-
.map { it.mediaTrackGroup }
1382-
.filter { it.length > 0 }
1383-
.map { it.getFormat(0).language }
1409+
.mapNotNull { group ->
1410+
group.mediaTrackGroup.takeIf { it.length > 0 }?.getFormat(0)?.language
1411+
}
13841412

13851413
// Find selected text track
13861414
val selectedTrack: Format? = textTracks
@@ -1449,7 +1477,7 @@ abstract class VideoPlayerUi protected constructor(
14491477
if (player.currentState == STATE_PLAYING && !isSomePopupMenuVisible) {
14501478
if (v == binding.playPauseButton ||
14511479
// Hide controls in fullscreen immediately
1452-
(v == binding.screenRotationButton && isFullscreen())
1480+
(v == binding.screenRotationButton && isFullscreen)
14531481
) {
14541482
hideControls(0, 0)
14551483
} else {
@@ -1463,7 +1491,7 @@ abstract class VideoPlayerUi protected constructor(
14631491
open fun onKeyDown(keyCode: Int): Boolean {
14641492
when (keyCode) {
14651493
KeyEvent.KEYCODE_BACK -> {
1466-
if (DeviceUtils.isTv(context) && isControlsVisible()) {
1494+
if (DeviceUtils.isTv(context) && isControlsVisible) {
14671495
hideControls(0, 0)
14681496
return true
14691497
}
@@ -1485,7 +1513,7 @@ abstract class VideoPlayerUi protected constructor(
14851513
return true
14861514
}
14871515

1488-
if (isControlsVisible()) {
1516+
if (isControlsVisible) {
14891517
hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME)
14901518
} else {
14911519
binding.playPauseButton.requestFocus()
@@ -1544,7 +1572,7 @@ abstract class VideoPlayerUi protected constructor(
15441572

15451573
// region Video size
15461574

1547-
protected fun setResizeMode(@AspectRatioFrameLayout.ResizeMode resizeMode: Int) {
1575+
protected fun setResizeMode(resizeMode: Int) {
15481576
binding.surfaceView.setResizeMode(resizeMode)
15491577
binding.resizeTextView.text = PlayerHelper.resizeTypeOf(context, resizeMode)
15501578
}
@@ -1610,8 +1638,6 @@ abstract class VideoPlayerUi protected constructor(
16101638
// region Getters
16111639

16121640
fun getBinding(): PlayerBinding = binding
1613-
1614-
fun getGestureDetector(): GestureDetector? = gestureDetector
16151641
// endregion
16161642

16171643
companion object {
@@ -1628,7 +1654,7 @@ abstract class VideoPlayerUi protected constructor(
16281654
val DPAD_CONTROLS_HIDE_TIME: Long = 7000 // 7 Seconds
16291655

16301656
@JvmField
1631-
val SEEK_OVERLAY_DURATION: Int = 450 // 450 millis
1657+
val SEEK_OVERLAY_DURATION: Long = 450 // 450 millis
16321658

16331659
// other constants (TODO remove playback speeds and use normal menu for popup, too)
16341660
private val PLAYBACK_SPEEDS = floatArrayOf(0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f)

0 commit comments

Comments
 (0)