diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 23e803fee..95b7a8970 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -45,6 +45,7 @@
import android.os.Build;
import android.os.Handler;
import android.provider.Settings;
+import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
@@ -310,12 +311,18 @@ public final class Player implements
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
private static final int POPUP_MENU_ID_CAPTION = 89;
private static final int POPUP_MENU_ID_AUDIO_TRACK = 99;
+ private static final int POPUP_MENU_ID_ASPECT_RATIO = 109;
private boolean isSomePopupMenuVisible = false;
private PopupMenu qualityPopupMenu;
private PopupMenu playbackSpeedPopupMenu;
private PopupMenu captionPopupMenu;
private PopupMenu audioTrackPopupMenu;
+ private PopupMenu aspectRatioPopupMenu;
+
+ // Aspect ratio forced by the user, 0 means "auto" (use the video's own aspect ratio)
+ private float forcedAspectRatio;
+ private float videoNaturalAspectRatio;
/*//////////////////////////////////////////////////////////////////////////
// Popup player
@@ -497,6 +504,9 @@ private void initViews(@NonNull final PlayerBinding playerBinding) {
binding.resizeTextView
.setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode()));
+ binding.aspectRatioTextView
+ .setText(PlayerHelper.aspectRatioNameOf(context, forcedAspectRatio));
+
binding.playbackSeekBar.getThumb()
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
binding.playbackSeekBar.getProgressDrawable()
@@ -509,6 +519,8 @@ private void initViews(@NonNull final PlayerBinding playerBinding) {
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
+ aspectRatioPopupMenu = new PopupMenu(themeWrapper, binding.aspectRatioTextView);
+ buildAspectRatioMenu();
binding.progressBarLoadingPanel.getIndeterminateDrawable()
.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
@@ -570,6 +582,7 @@ private void initListeners() {
binding.captionTextView.setOnClickListener(this);
binding.audioTrackTextView.setOnClickListener(this);
binding.resizeTextView.setOnClickListener(this);
+ binding.aspectRatioTextView.setOnClickListener(this);
binding.playbackLiveSync.setOnClickListener(this);
playerGestureListener = new PlayerGestureListener(this, service);
@@ -1145,6 +1158,7 @@ private void setupElementsVisibility() {
binding.fullScreenButton.setVisibility(View.VISIBLE);
binding.screenRotationButton.setVisibility(View.GONE);
binding.resizeTextView.setVisibility(View.GONE);
+ binding.aspectRatioTextView.setVisibility(View.GONE);
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
binding.queueButton.setVisibility(View.GONE);
binding.segmentsButton.setVisibility(View.GONE);
@@ -1171,6 +1185,7 @@ private void setupElementsVisibility() {
binding.fullScreenButton.setVisibility(View.GONE);
setupScreenRotationButton();
binding.resizeTextView.setVisibility(View.VISIBLE);
+ binding.aspectRatioTextView.setVisibility(View.VISIBLE);
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE);
binding.moreOptionsButton.setVisibility(View.VISIBLE);
binding.topControls.setOrientation(LinearLayout.VERTICAL);
@@ -1237,6 +1252,7 @@ private void setupElementsSize() {
binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
+ binding.aspectRatioTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
}
private void showHideKodiButton() {
@@ -3529,6 +3545,11 @@ private void onMetadataChanged(@NonNull final StreamInfo info) {
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
}
+ // a forced aspect ratio is a per-video correction, don't carry it over to the next one
+ if (forcedAspectRatio > 0) {
+ setPlaybackAspectRatio(0.0f);
+ }
+
initThumbnail(info.getThumbnailUrl());
registerStreamViewed();
updateStreamRelatedViews();
@@ -4178,6 +4199,9 @@ private void closeAllPopupMenus() {
if (captionPopupMenu != null) {
captionPopupMenu.dismiss();
}
+ if (aspectRatioPopupMenu != null) {
+ aspectRatioPopupMenu.dismiss();
+ }
isSomePopupMenuVisible = false;
}
//endregion
@@ -4363,6 +4387,8 @@ public void onClick(final View v) {
}
if (v.getId() == binding.resizeTextView.getId()) {
onResizeClicked();
+ } else if (v.getId() == binding.aspectRatioTextView.getId()) {
+ onAspectRatioClicked();
} else if (v.getId() == binding.captionTextView.getId()) {
onCaptionClicked();
} else if (v.getId() == binding.audioTrackTextView.getId()) {
@@ -4603,6 +4629,85 @@ void onResizeClicked() {
}
}
+ private void buildAspectRatioMenu() {
+ if (aspectRatioPopupMenu == null) {
+ return;
+ }
+ aspectRatioPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_ASPECT_RATIO);
+ aspectRatioPopupMenu.setOnDismissListener(this);
+
+ final MenuItem autoItem = aspectRatioPopupMenu.getMenu().add(POPUP_MENU_ID_ASPECT_RATIO,
+ 0, Menu.NONE, R.string.aspect_ratio_auto);
+ autoItem.setOnMenuItemClickListener(menuItem -> {
+ setPlaybackAspectRatio(0.0f);
+ return true;
+ });
+
+ for (int i = 0; i < PlayerHelper.ASPECT_RATIO_VALUES.length; i++) {
+ final float ratio = PlayerHelper.ASPECT_RATIO_VALUES[i];
+ final MenuItem ratioItem = aspectRatioPopupMenu.getMenu().add(
+ POPUP_MENU_ID_ASPECT_RATIO, i + 1, Menu.NONE,
+ PlayerHelper.ASPECT_RATIO_LABELS[i]);
+ ratioItem.setOnMenuItemClickListener(menuItem -> {
+ setPlaybackAspectRatio(ratio);
+ return true;
+ });
+ }
+
+ final MenuItem customItem = aspectRatioPopupMenu.getMenu().add(POPUP_MENU_ID_ASPECT_RATIO,
+ PlayerHelper.ASPECT_RATIO_VALUES.length + 1, Menu.NONE,
+ R.string.aspect_ratio_custom);
+ customItem.setOnMenuItemClickListener(menuItem -> {
+ openCustomAspectRatioDialog();
+ return true;
+ });
+ }
+
+ private void onAspectRatioClicked() {
+ if (DEBUG) {
+ Log.d(TAG, "onAspectRatioClicked() called");
+ }
+ aspectRatioPopupMenu.show();
+ isSomePopupMenuVisible = true;
+ }
+
+ private void setPlaybackAspectRatio(final float aspectRatio) {
+ forcedAspectRatio = aspectRatio;
+ binding.aspectRatioTextView.setText(PlayerHelper.aspectRatioNameOf(context, aspectRatio));
+
+ final float effectiveRatio = aspectRatio > 0 ? aspectRatio : videoNaturalAspectRatio;
+ if (effectiveRatio > 0) {
+ binding.surfaceView.setAspectRatio(effectiveRatio);
+ }
+ }
+
+ private void openCustomAspectRatioDialog() {
+ final AppCompatActivity activity = getParentActivity();
+ if (activity == null) {
+ return;
+ }
+ final EditText input = new EditText(activity);
+ input.setHint(R.string.aspect_ratio_custom_hint);
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ if (forcedAspectRatio > 0) {
+ input.setText(PlayerHelper.aspectRatioNameOf(context, forcedAspectRatio));
+ }
+ new AlertDialog.Builder(activity)
+ .setTitle(R.string.aspect_ratio_custom_title)
+ .setView(input)
+ .setPositiveButton(R.string.ok, (dialog, which) -> {
+ final float ratio = PlayerHelper.parseAspectRatio(input.getText().toString());
+ if (ratio > 0) {
+ setPlaybackAspectRatio(ratio);
+ } else {
+ Toast.makeText(context, R.string.aspect_ratio_invalid, Toast.LENGTH_SHORT)
+ .show();
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
@Override // exoplayer listener
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
if (DEBUG) {
@@ -4613,7 +4718,9 @@ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
+ "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
}
- binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
+ videoNaturalAspectRatio = ((float) videoSize.width) / videoSize.height;
+ binding.surfaceView.setAspectRatio(forcedAspectRatio > 0
+ ? forcedAspectRatio : videoNaturalAspectRatio);
isVerticalVideo = videoSize.width < videoSize.height;
if (globalScreenOrientationLocked(context)
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index 91258be87..15cd24368 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -552,6 +552,56 @@ public static int nextRepeatMode(@RepeatMode final int repeatMode) {
}
}
+ /**
+ * Aspect ratios selectable in the player. Indices in both arrays match.
+ */
+ public static final String[] ASPECT_RATIO_LABELS = {"1:1", "4:3", "16:9", "18:9", "21:9"};
+ public static final float[] ASPECT_RATIO_VALUES = {
+ 1.0f, 4.0f / 3.0f, 16.0f / 9.0f, 18.0f / 9.0f, 21.0f / 9.0f};
+
+ public static String aspectRatioNameOf(@NonNull final Context context,
+ final float aspectRatio) {
+ if (aspectRatio <= 0.0f) {
+ return context.getString(R.string.aspect_ratio_auto);
+ }
+ for (int i = 0; i < ASPECT_RATIO_VALUES.length; i++) {
+ if (Math.abs(ASPECT_RATIO_VALUES[i] - aspectRatio) < 0.001f) {
+ return ASPECT_RATIO_LABELS[i];
+ }
+ }
+ return String.format(Locale.US, "%.2f", aspectRatio);
+ }
+
+ /**
+ * Parses a user-entered aspect ratio like {@code "16:9"}, {@code "16/9"} or {@code "1.78"}.
+ *
+ * @return the ratio as a float, or {@code 0} if the input could not be parsed
+ */
+ public static float parseAspectRatio(@Nullable final String input) {
+ if (input == null) {
+ return 0.0f;
+ }
+ final String trimmed = input.trim();
+ try {
+ final String[] parts = trimmed.split("[:/]");
+ if (parts.length == 2) {
+ final float width = Float.parseFloat(parts[0].trim());
+ final float height = Float.parseFloat(parts[1].trim());
+ if (width > 0 && height > 0) {
+ return width / height;
+ }
+ } else if (parts.length == 1) {
+ final float ratio = Float.parseFloat(trimmed.replace(',', '.'));
+ if (ratio > 0) {
+ return ratio;
+ }
+ }
+ } catch (final NumberFormatException ignored) {
+ // fall through to invalid
+ }
+ return 0.0f;
+ }
+
@ResizeMode
public static int retrieveResizeModeFromPrefs(final Player player) {
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),
diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml
index 8db2007a0..4780eda62 100644
--- a/app/src/main/res/layout/player.xml
+++ b/app/src/main/res/layout/player.xml
@@ -293,6 +293,20 @@
tools:ignore="HardcodedText,RtlHardcoded"
tools:text="FIT" />
+
+
Fit
Fill
Zoom
+Auto
+Custom…
+Custom aspect ratio
+e.g. 16:9 or 1.78
+Invalid aspect ratio
Auto-generated
Captions