diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java index 0bb01b10d..5ef52cefd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -65,7 +65,9 @@ import org.schabi.newpipe.databinding.PlayerBinding; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.views.ChaptersSeekBar; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.player.Player; @@ -85,6 +87,7 @@ import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.views.player.PlayerFastSeekOverlay; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -147,6 +150,9 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder = new SeekbarPreviewThumbnailHolder(); + @NonNull + private List currentChapters = Collections.emptyList(); + /*////////////////////////////////////////////////////////////////////////// // Constructor, setup, destroy @@ -589,6 +595,14 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa binding.currentSeekbarPreviewThumbnail, binding.subtitleView::getWidth); + // Chapter title tooltip + if (!currentChapters.isEmpty()) { + final StreamSegment chapter = getChapterAtMs(progress); + if (chapter != null && chapter.getTitle() != null) { + binding.currentChapterTitle.setText(chapter.getTitle()); + } + } + adjustSeekbarPreviewContainer(); } @@ -642,6 +656,10 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa AnimationType.SCALE_AND_ALPHA); animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION, AnimationType.SCALE_AND_ALPHA); + if (!currentChapters.isEmpty()) { + animate(binding.currentChapterTitle, true, DEFAULT_CONTROLS_DURATION, + AnimationType.SCALE_AND_ALPHA); + } } @Override // seekbar listener @@ -658,6 +676,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA); animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA); + animate(binding.currentChapterTitle, false, 200, AnimationType.SCALE_AND_ALPHA); if (player.getCurrentState() == STATE_PAUSED_SEEK) { player.changeState(STATE_BUFFERING); @@ -668,6 +687,25 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa showControlsThenHide(); } + + /** + * Returns the chapter active at the given playback position, or {@code null} if + * {@code currentChapters} is empty. + * + * @param positionMs playback position in milliseconds + * @return the {@link StreamSegment} whose window contains {@code positionMs} + */ + @Nullable + private StreamSegment getChapterAtMs(final long positionMs) { + StreamSegment result = null; + for (final StreamSegment seg : currentChapters) { + if (seg.getStartTimeSeconds() * 1000L > positionMs) { + break; + } + result = seg; + } + return result; + } //endregion @@ -1036,6 +1074,22 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa binding.channelTextView.setText(info.getUploaderName()); this.seekbarPreviewThumbnailHolder.resetFrom(player.getContext(), info.getPreviewFrames()); + + // Chapter markers on seekbar + currentChapters = info.getStreamSegments() != null + ? info.getStreamSegments() : Collections.emptyList(); + Log.d(TAG, "onMetadataChanged: seekBarClass=" + + binding.playbackSeekBar.getClass().getSimpleName() + + " segments=" + currentChapters.size() + + " duration=" + info.getDuration()); + if (binding.playbackSeekBar instanceof ChaptersSeekBar) { + ((ChaptersSeekBar) binding.playbackSeekBar) + .setChapters(currentChapters, info.getDuration()); + } else { + Log.e(TAG, "onMetadataChanged: playbackSeekBar is NOT a ChaptersSeekBar! " + + "Check that player.xml was rebuilt."); + } + binding.currentChapterTitle.setVisibility(View.GONE); } private void updateStreamRelatedViews() { diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java index 8176a9aef..651d2f373 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -33,7 +33,7 @@ import org.schabi.newpipe.util.DeviceUtils; * (onStartTrackingTouch/onStopTrackingTouch), so existing code does not need to be changed to * work with it. */ -public final class FocusAwareSeekBar extends AppCompatSeekBar { +public class FocusAwareSeekBar extends AppCompatSeekBar { private NestedListener listener; private ViewTreeObserver treeObserver; diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index a6a0884c7..9c6ae0a40 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -415,6 +415,23 @@ android:orientation="vertical" android:paddingBottom="12dp"> + + -