@@ -20,7 +20,6 @@ import android.view.View
2020import android.widget.LinearLayout
2121import android.widget.RelativeLayout
2222import android.widget.SeekBar
23- import androidx.annotation.OptIn
2423import androidx.appcompat.content.res.AppCompatResources
2524import androidx.appcompat.view.ContextThemeWrapper
2625import 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