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