Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import android.os.Looper;
import android.util.Log;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
Expand Down Expand Up @@ -66,7 +67,9 @@
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;
Expand All @@ -86,6 +89,7 @@
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;
Expand Down Expand Up @@ -148,6 +152,11 @@ private enum PlayButtonAction {
private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
new SeekbarPreviewThumbnailHolder();

@NonNull
private List<StreamSegment> currentChapters = Collections.emptyList();
@Nullable
private StreamSegment lastChapterForHaptic = null;


/*//////////////////////////////////////////////////////////////////////////
// Constructor, setup, destroy
Expand Down Expand Up @@ -586,6 +595,18 @@ public void onProgressChanged(final SeekBar seekBar, final int progress,
binding.currentSeekbarPreviewThumbnail,
binding.subtitleView::getWidth);

// Chapter title tooltip + haptic feedback at chapter boundaries
if (!currentChapters.isEmpty()) {
final StreamSegment chapter = getChapterAtMs(progress);
if (chapter != null && chapter.getTitle() != null) {
binding.currentChapterTitle.setText(chapter.getTitle());
}
if (chapter != lastChapterForHaptic) {
lastChapterForHaptic = chapter;
seekBar.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
}
}

adjustSeekbarPreviewContainer();
}

Expand Down Expand Up @@ -639,6 +660,10 @@ public void onStartTrackingTouch(final SeekBar seekBar) {
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
Expand All @@ -655,6 +680,7 @@ public void onStopTrackingTouch(final SeekBar seekBar) {
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);
Expand All @@ -665,6 +691,25 @@ public void onStopTrackingTouch(final SeekBar seekBar) {

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


Expand Down Expand Up @@ -1021,6 +1066,14 @@ public void onMetadataChanged(@NonNull final StreamInfo info) {
binding.channelTextView.setText(info.getUploaderName());

this.seekbarPreviewThumbnailHolder.resetFrom(player.getContext(), info.getPreviewFrames());

// Chapter markers on seekbar
currentChapters = info.getStreamSegments() != null
? info.getStreamSegments() : Collections.emptyList();
lastChapterForHaptic = null;
((ChaptersSeekBar) binding.playbackSeekBar)
.setChapters(currentChapters, info.getDuration());
binding.currentChapterTitle.setVisibility(View.GONE);
}

private void updateStreamRelatedViews() {
Expand Down
125 changes: 125 additions & 0 deletions app/src/main/java/org/schabi/newpipe/views/ChaptersSeekBar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (C) NewPipe Contributors
* ChaptersSeekBar.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.views;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.schabi.newpipe.extractor.stream.StreamSegment;

import java.util.Collections;
import java.util.List;

/**
* A {@link FocusAwareSeekBar} that renders narrow transparent gaps at chapter boundaries,
* giving the seekbar a segmented "chopped" appearance.
* Call {@link #setChapters(List, long)} whenever a new stream loads.
*/
public final class ChaptersSeekBar extends FocusAwareSeekBar {

private static final float GAP_WIDTH_DP = 2f;

private final Paint gapPaint = new Paint();

@NonNull private List<StreamSegment> chapters = Collections.emptyList();
private long durationSeconds = 0;

public ChaptersSeekBar(@NonNull final Context context) {
super(context);
init();
}

public ChaptersSeekBar(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
init();
}

public ChaptersSeekBar(@NonNull final Context context,
@Nullable final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
gapPaint.setStyle(Paint.Style.FILL);
gapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}

/**
* Stores chapter data for rendering segment gaps.
*
* @param newChapters list of {@link StreamSegment}s; may be empty but never null
* @param newDurationSecs total duration in seconds; used to compute fractional positions
*/
public void setChapters(@NonNull final List<StreamSegment> newChapters,
final long newDurationSecs) {
chapters = newChapters;
durationSeconds = newDurationSecs;
invalidate();
}

@Override
protected void onDraw(@NonNull final Canvas canvas) {
if (chapters.isEmpty() || durationSeconds <= 0) {
super.onDraw(canvas);
return;
}

// Draw the seekbar into an offscreen layer so CLEAR mode can punch transparent gaps
final int sc = canvas.saveLayer(null, null);
super.onDraw(canvas);

final float density = getResources().getDisplayMetrics().density;
final float gapHalfWidth = (GAP_WIDTH_DP * density) / 2f;
final int paddingLeft = getPaddingLeft();
final float trackWidth = getWidth() - paddingLeft - getPaddingRight();

if (trackWidth > 0) {
for (final StreamSegment seg : chapters) {
final int startSec = seg.getStartTimeSeconds();
// Skip the very first position and anything at or past the end
if (startSec <= 0 || startSec >= durationSeconds) {
continue;
}
final float x = paddingLeft + (startSec / (float) durationSeconds) * trackWidth;
canvas.drawRect(x - gapHalfWidth, 0, x + gapHalfWidth, getHeight(), gapPaint);
}
}

canvas.restoreToCount(sc);

// Redraw the thumb on top so it visually overlaps the gaps
final Drawable thumb = getThumb();
if (thumb != null) {
final int thumbSave = canvas.save();
canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop());
thumb.draw(canvas);
canvas.restoreToCount(thumbSave);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
* (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;
Expand Down
19 changes: 18 additions & 1 deletion app/src/main/res/layout/player.xml
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,23 @@
android:orientation="vertical"
android:paddingBottom="12dp">

<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/currentChapterTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#60000000"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:paddingBottom="2dp"
android:textColor="@android:color/white"
android:textSize="12sp"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:text="Introduction"
tools:visibility="visible" />

<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/currentDisplaySeek"
android:layout_width="wrap_content"
Expand Down Expand Up @@ -467,7 +484,7 @@
tools:text="1:06:29" />


<org.schabi.newpipe.views.FocusAwareSeekBar
<org.schabi.newpipe.views.ChaptersSeekBar
android:id="@+id/playbackSeekBar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"
Expand Down
Loading