Skip to content

Commit ce77610

Browse files
committed
Refactor VideoPlayerUi to improve accessibility for Java subclasses and enhance chapter marker handling
1 parent 9b3975e commit ce77610

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
@@ -21,7 +21,6 @@ import android.view.View
2121
import android.widget.LinearLayout
2222
import android.widget.RelativeLayout
2323
import android.widget.SeekBar
24-
import androidx.annotation.OptIn
2524
import androidx.appcompat.content.res.AppCompatResources
2625
import androidx.appcompat.view.ContextThemeWrapper
2726
import androidx.appcompat.widget.AppCompatImageButton
@@ -102,7 +101,7 @@ abstract class VideoPlayerUi protected constructor(
102101
// region Views
103102

104103
@JvmField
105-
protected var binding: PlayerBinding = playerBinding
104+
var binding: PlayerBinding = playerBinding
106105

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

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

116115
@JvmField
117-
protected var isSomePopupMenuVisible = false
116+
var isSomePopupMenuVisible = false
118117

119118
private var qualityPopupMenu: PopupMenu? = null
120119
private var audioTrackPopupMenu: PopupMenu? = null
@@ -126,7 +125,7 @@ abstract class VideoPlayerUi protected constructor(
126125

127126
// Gestures
128127

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

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

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

183186
protected open fun initListeners() {
184187
binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked))
@@ -193,7 +196,7 @@ abstract class VideoPlayerUi protected constructor(
193196
binding.playbackLiveSync.setOnClickListener(makeOnClickListener(player::seekToDefault))
194197

195198
playerGestureListener = buildGestureListener()
196-
gestureDetector = GestureDetector(context, playerGestureListener)
199+
gestureDetector = GestureDetector(context, playerGestureListener!!)
197200
binding.root.setOnTouchListener(playerGestureListener)
198201

199202
binding.repeatButton.setOnClickListener { onRepeatClicked() }
@@ -393,6 +396,14 @@ abstract class VideoPlayerUi protected constructor(
393396

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

398409
abstract fun removeViewFromParent()
@@ -498,18 +509,33 @@ abstract class VideoPlayerUi protected constructor(
498509
if (duration != binding.playbackSeekBar.max) {
499510
setVideoDurationToControls(duration)
500511
}
512+
513+
// If chapter metadata callback was missed, recover from currentStreamInfo.
514+
if (currentChapters.isEmpty()) {
515+
player.currentStreamInfo.ifPresent { info ->
516+
val streamSegments = info.streamSegments ?: emptyList()
517+
if (streamSegments.isNotEmpty()) {
518+
currentChapters = streamSegments
519+
lastChapterForHaptic = null
520+
(binding.playbackSeekBar as? ChaptersSeekBar)
521+
?.setChapters(currentChapters, info.duration)
522+
}
523+
}
524+
}
525+
501526
if (player.currentState != STATE_PAUSED) {
502527
updatePlayBackElementsCurrentDuration(currentProgress)
503528
}
504529
if (player.isLoading || bufferPercent > 90) {
505530
binding.playbackSeekBar.secondaryProgress =
506531
(binding.playbackSeekBar.max * (bufferPercent.toFloat() / 100)).toInt()
507532
}
533+
508534
if (DEBUG && bufferPercent % 20 == 0) { // Limit log
509535
Log.d(
510536
TAG,
511537
"notifyProgressUpdateToListeners() called with: " +
512-
"isVisible = ${isControlsVisible()}, " +
538+
"isVisible = $isControlsVisible, " +
513539
"currentProgress = [$currentProgress], " +
514540
"duration = [$duration], bufferPercent = [$bufferPercent]"
515541
)
@@ -527,7 +553,7 @@ abstract class VideoPlayerUi protected constructor(
527553
if (player.currentState != STATE_PAUSED_SEEK) {
528554
binding.playbackSeekBar.progress = currentProgress
529555
}
530-
binding.playbackCurrentTime.text = getTimeString(currentProgress)
556+
binding.playbackCurrentTime.text = getTimeString(currentProgress.toLong())
531557
}
532558

533559
/**
@@ -536,7 +562,7 @@ abstract class VideoPlayerUi protected constructor(
536562
* @param duration the video duration, in milliseconds
537563
*/
538564
private fun setVideoDurationToControls(duration: Int) {
539-
binding.playbackEndTime.text = getTimeString(duration)
565+
binding.playbackEndTime.text = getTimeString(duration.toLong())
540566

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

562-
binding.currentDisplaySeek.text = getTimeString(progress)
588+
binding.currentDisplaySeek.text = getTimeString(progress.toLong())
563589

564590
// Seekbar Preview Thumbnail
565591
SeekbarPreviewThumbnailHelper
@@ -646,7 +672,7 @@ abstract class VideoPlayerUi protected constructor(
646672
player.exoPlayer.play()
647673
}
648674

649-
binding.playbackCurrentTime.text = getTimeString(seekBar.progress)
675+
binding.playbackCurrentTime.text = getTimeString(seekBar.progress.toLong())
650676
binding.currentDisplaySeek.animate(false, 200, AnimationType.SCALE_AND_ALPHA)
651677
binding.currentSeekbarPreviewThumbnail.animate(false, 200, AnimationType.SCALE_AND_ALPHA)
652678
binding.currentChapterTitle.animate(false, 200, AnimationType.SCALE_AND_ALPHA)
@@ -682,7 +708,8 @@ abstract class VideoPlayerUi protected constructor(
682708

683709
// region Controls showing / hiding
684710

685-
fun isControlsVisible(): Boolean = binding.playbackControlRoot.visibility == View.VISIBLE
711+
val isControlsVisible: Boolean
712+
get() = binding.playbackControlRoot.visibility == View.VISIBLE
686713

687714
fun showControlsThenHide() {
688715
if (DEBUG) {
@@ -773,10 +800,9 @@ abstract class VideoPlayerUi protected constructor(
773800
return false
774801
}
775802

776-
open fun isFullscreen(): Boolean {
803+
open val isFullscreen: Boolean
777804
// only MainPlayerUi can be in fullscreen, so overridden there
778-
return false
779-
}
805+
get() = false
780806

781807
/**
782808
* Update the play/pause button to reflect the action that will be performed when clicked.
@@ -808,7 +834,7 @@ abstract class VideoPlayerUi protected constructor(
808834
override fun onPrepared() {
809835
super.onPrepared()
810836
setVideoDurationToControls(player.exoPlayer.duration.toInt())
811-
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed)
837+
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed.toDouble())
812838
}
813839

814840
override fun onBlocked() {
@@ -866,7 +892,7 @@ abstract class VideoPlayerUi protected constructor(
866892

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

@@ -983,7 +1009,7 @@ abstract class VideoPlayerUi protected constructor(
9831009

9841010
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
9851011
super.onPlaybackParametersChanged(playbackParameters)
986-
binding.playbackSpeed.text = formatSpeed(playbackParameters.speed)
1012+
binding.playbackSpeed.text = formatSpeed(playbackParameters.speed.toDouble())
9871013
}
9881014

9891015
override fun onRenderedFirstFrame() {
@@ -1006,10 +1032,13 @@ abstract class VideoPlayerUi protected constructor(
10061032
seekbarPreviewThumbnailHolder.resetFrom(player.context, info.previewFrames)
10071033

10081034
// Chapter markers on seekbar
1009-
currentChapters = info.streamSegments ?: emptyList()
1035+
val rawSegments = info.streamSegments
1036+
currentChapters = rawSegments ?: emptyList()
10101037
lastChapterForHaptic = null
1011-
(binding.playbackSeekBar as ChaptersSeekBar)
1012-
.setChapters(currentChapters, info.duration)
1038+
val seekBar = binding.playbackSeekBar
1039+
if (seekBar is ChaptersSeekBar) {
1040+
seekBar.setChapters(currentChapters, info.duration)
1041+
}
10131042
binding.currentChapterTitle.visibility = View.GONE
10141043
}
10151044

@@ -1135,10 +1164,10 @@ abstract class VideoPlayerUi protected constructor(
11351164
POPUP_MENU_ID_PLAYBACK_SPEED,
11361165
i,
11371166
Menu.NONE,
1138-
formatSpeed(PLAYBACK_SPEEDS[i])
1167+
formatSpeed(PLAYBACK_SPEEDS[i].toDouble())
11391168
)
11401169
}
1141-
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed)
1170+
binding.playbackSpeed.text = formatSpeed(player.playbackSpeed.toDouble())
11421171
playbackSpeedPopupMenu!!.setOnMenuItemClickListener(this)
11431172
playbackSpeedPopupMenu!!.setOnDismissListener(this)
11441173
}
@@ -1288,7 +1317,7 @@ abstract class VideoPlayerUi protected constructor(
12881317
val speedIndex = menuItem.itemId
12891318
val speed = PLAYBACK_SPEEDS[speedIndex]
12901319
player.setPlaybackSpeed(speed)
1291-
binding.playbackSpeed.text = formatSpeed(speed)
1320+
binding.playbackSpeed.text = formatSpeed(speed.toDouble())
12921321
false
12931322
}
12941323

@@ -1361,7 +1390,6 @@ abstract class VideoPlayerUi protected constructor(
13611390
isSomePopupMenuVisible = true
13621391
}
13631392

1364-
fun isSomePopupMenuVisible(): Boolean = isSomePopupMenuVisible
13651393
// endregion
13661394

13671395
// region Captions (text tracks)
@@ -1377,11 +1405,11 @@ abstract class VideoPlayerUi protected constructor(
13771405
}
13781406

13791407
// Extract all loaded languages
1380-
val textTracks = currentTracks.groups.filter { it.type == C.TRACK_TYPE_TEXT }
1408+
val textTracks: List<Tracks.Group> = currentTracks.groups.filter { it.type == C.TRACK_TYPE_TEXT }
13811409
val availableLanguages = textTracks
1382-
.map { it.mediaTrackGroup }
1383-
.filter { it.length > 0 }
1384-
.map { it.getFormat(0).language }
1410+
.mapNotNull { group ->
1411+
group.mediaTrackGroup.takeIf { it.length > 0 }?.getFormat(0)?.language
1412+
}
13851413

13861414
// Find selected text track
13871415
val selectedTrack: Format? = textTracks
@@ -1450,7 +1478,7 @@ abstract class VideoPlayerUi protected constructor(
14501478
if (player.currentState == STATE_PLAYING && !isSomePopupMenuVisible) {
14511479
if (v == binding.playPauseButton ||
14521480
// Hide controls in fullscreen immediately
1453-
(v == binding.screenRotationButton && isFullscreen())
1481+
(v == binding.screenRotationButton && isFullscreen)
14541482
) {
14551483
hideControls(0, 0)
14561484
} else {
@@ -1464,7 +1492,7 @@ abstract class VideoPlayerUi protected constructor(
14641492
open fun onKeyDown(keyCode: Int): Boolean {
14651493
when (keyCode) {
14661494
KeyEvent.KEYCODE_BACK -> {
1467-
if (DeviceUtils.isTv(context) && isControlsVisible()) {
1495+
if (DeviceUtils.isTv(context) && isControlsVisible) {
14681496
hideControls(0, 0)
14691497
return true
14701498
}
@@ -1486,7 +1514,7 @@ abstract class VideoPlayerUi protected constructor(
14861514
return true
14871515
}
14881516

1489-
if (isControlsVisible()) {
1517+
if (isControlsVisible) {
14901518
hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME)
14911519
} else {
14921520
binding.playPauseButton.requestFocus()
@@ -1545,7 +1573,7 @@ abstract class VideoPlayerUi protected constructor(
15451573

15461574
// region Video size
15471575

1548-
protected fun setResizeMode(@AspectRatioFrameLayout.ResizeMode resizeMode: Int) {
1576+
protected fun setResizeMode(resizeMode: Int) {
15491577
binding.surfaceView.setResizeMode(resizeMode)
15501578
binding.resizeTextView.text = PlayerHelper.resizeTypeOf(context, resizeMode)
15511579
}
@@ -1615,8 +1643,6 @@ abstract class VideoPlayerUi protected constructor(
16151643
// region Getters
16161644

16171645
fun getBinding(): PlayerBinding = binding
1618-
1619-
fun getGestureDetector(): GestureDetector? = gestureDetector
16201646
// endregion
16211647

16221648
companion object {
@@ -1633,7 +1659,7 @@ abstract class VideoPlayerUi protected constructor(
16331659
val DPAD_CONTROLS_HIDE_TIME: Long = 7000 // 7 Seconds
16341660

16351661
@JvmField
1636-
val SEEK_OVERLAY_DURATION: Int = 450 // 450 millis
1662+
val SEEK_OVERLAY_DURATION: Long = 450 // 450 millis
16371663

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

0 commit comments

Comments
 (0)