Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Player seekbar thumbnail preview #6434

Merged
merged 11 commits into from
Jul 19, 2021
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:c38a06e8dcd9c206a52b622704b138b78d633274'
implementation 'com.github.litetex:NewPipeExtractor:playerSeekbarPreview-SNAPSHOT'

/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
Expand Down
73 changes: 70 additions & 3 deletions app/src/main/java/org/schabi/newpipe/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
Expand Down Expand Up @@ -123,6 +124,8 @@
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.ListHelper;
Expand Down Expand Up @@ -379,6 +382,8 @@ public final class Player implements
@NonNull private final SharedPreferences prefs;
@NonNull private final HistoryRecordManager recordManager;

@NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
new SeekbarPreviewThumbnailHolder();


/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -1676,12 +1681,67 @@ private Disposable getProgressUpdateDisposable() {
@Override // seekbar listener
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
if (DEBUG && fromUser) {
// Currently we don't need method execution when fromUser is false
if (!fromUser) {
return;
}
if (DEBUG) {
Log.d(TAG, "onProgressChanged() called with: "
+ "seekBar = [" + seekBar + "], progress = [" + progress + "]");
}
if (fromUser) {
binding.currentDisplaySeek.setText(getTimeString(progress));

Stypox marked this conversation as resolved.
Show resolved Hide resolved
binding.currentDisplaySeek.setText(getTimeString(progress));

// Seekbar Preview Thumbnail
SeekbarPreviewThumbnailHelper
.tryResizeAndSetSeekbarPreviewThumbnail(
getContext(),
seekbarPreviewThumbnailHolder.getBitmapAt(progress),
binding.currentSeekbarPreviewThumbnail,
binding.subtitleView::getWidth);

adjustSeekbarPreviewContainer();
}

private void adjustSeekbarPreviewContainer() {
try {
// Should only be required when an error occurred before
// and the layout was positioned in the center
binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY);

// Calculate the current left position of seekbar progress in px
// More info: https://stackoverflow.com/q/20493577
final int currentSeekbarLeft =
binding.playbackSeekBar.getLeft()
+ binding.playbackSeekBar.getPaddingLeft()
+ binding.playbackSeekBar.getThumb().getBounds().left;

// Calculate the (unchecked) left position of the container
final int uncheckedContainerLeft =
currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2);

// Fix the position so it's within the boundaries
final int checkedContainerLeft =
Math.max(
Math.min(
uncheckedContainerLeft,
// Max left
binding.playbackWindowRoot.getWidth()
- binding.seekbarPreviewContainer.getWidth()
),
0 // Min left
);

// See also: https://stackoverflow.com/a/23249734
final LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(
binding.seekbarPreviewContainer.getLayoutParams());
params.setMarginStart(checkedContainerLeft);
binding.seekbarPreviewContainer.setLayoutParams(params);
} catch (final Exception ex) {
Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex);
// Fallback - position in the middle
binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER);
}
}

Expand All @@ -1702,6 +1762,8 @@ public void onStartTrackingTouch(final SeekBar seekBar) {
showControls(0);
animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SCALE_AND_ALPHA);
animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SCALE_AND_ALPHA);
}

@Override // seekbar listener
Expand All @@ -1717,6 +1779,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);

if (currentState == STATE_PAUSED_SEEK) {
changeState(STATE_BUFFERING);
Expand Down Expand Up @@ -2866,6 +2929,10 @@ private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
binding.titleTextView.setText(tag.getMetadata().getName());
binding.channelTextView.setText(tag.getMetadata().getUploaderName());

this.seekbarPreviewThumbnailHolder.resetFrom(
this.getContext(),
tag.getMetadata().getPreviewFrames());

NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
notifyMetadataUpdateToListeners();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.schabi.newpipe.player.seekbarpreview;

import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;

import org.schabi.newpipe.R;
import org.schabi.newpipe.util.DeviceUtils;

import java.lang.annotation.Retention;
import java.util.Objects;
import java.util.Optional;
import java.util.function.IntSupplier;

import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.HIGH_QUALITY;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.LOW_QUALITY;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.NONE;

/**
* Helper for the seekbar preview.
*/
public final class SeekbarPreviewThumbnailHelper {

// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
// or it fails with an IllegalArgumentException
// https://stackoverflow.com/a/54744028
public static final String TAG = "SeekbarPrevThumbHelper";

private SeekbarPreviewThumbnailHelper() {
// No impl pls
}

@Retention(SOURCE)
@IntDef({HIGH_QUALITY, LOW_QUALITY,
NONE})
public @interface SeekbarPreviewThumbnailType {
int HIGH_QUALITY = 0;
int LOW_QUALITY = 1;
int NONE = 2;
}

////////////////////////////////////////////////////////////////////////////
// Settings Resolution
///////////////////////////////////////////////////////////////////////////

@SeekbarPreviewThumbnailType
public static int getSeekbarPreviewThumbnailType(@NonNull final Context context) {
final String type = PreferenceManager.getDefaultSharedPreferences(context).getString(
context.getString(R.string.seekbar_preview_thumbnail_key), "");
if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_none))) {
return NONE;
} else if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_low_quality))) {
return LOW_QUALITY;
} else {
return HIGH_QUALITY; // default
}
}

public static void tryResizeAndSetSeekbarPreviewThumbnail(
@NonNull final Context context,
@NonNull final Optional<Bitmap> optPreviewThumbnail,
@NonNull final ImageView currentSeekbarPreviewThumbnail,
@NonNull final IntSupplier baseViewWidthSupplier) {

if (!optPreviewThumbnail.isPresent()) {
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
return;
}

currentSeekbarPreviewThumbnail.setVisibility(View.VISIBLE);
final Bitmap srcBitmap = optPreviewThumbnail.get();

// Resize original bitmap
try {
Objects.requireNonNull(srcBitmap);

final int srcWidth = srcBitmap.getWidth() > 0 ? srcBitmap.getWidth() : 1;
final int newWidth = Math.max(
Math.min(
// Use 1/4 of the width for the preview
Math.round(baseViewWidthSupplier.getAsInt() / 4f),
// Scaling more than that factor looks really pixelated -> max
Math.round(srcWidth * 2.5f)
),
// Min width = 10dp
DeviceUtils.dpToPx(10, context)
);

final float scaleFactor = (float) newWidth / srcWidth;
final int newHeight = (int) (srcBitmap.getHeight() * scaleFactor);

currentSeekbarPreviewThumbnail.setImageBitmap(
Bitmap.createScaledBitmap(srcBitmap, newWidth, newHeight, true));
} catch (final Exception ex) {
Log.e(TAG, "Failed to resize and set seekbar preview thumbnail", ex);
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
} finally {
srcBitmap.recycle();
}
}
}
Loading