From 3d9ff44b57c3496eaa423f26fcb4005fe63e7829 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 12 Jun 2026 18:19:25 +0300 Subject: [PATCH] UI: add aspect ratio selector to video player Adds a button next to the resize mode toggle that lets the user force the video aspect ratio: Auto (video's own), 1:1, 4:3, 16:9, 18:9, 21:9, or a custom value entered as '16:9', '16/9' or '1.78'. Useful for videos that were uploaded with wrong proportions (e.g. 4:3 content stretched to 16:9). The choice is per-video: it resets to Auto when playback moves to a different stream, but survives quality changes of the same stream. Hidden in the popup player, like the resize mode button. --- .../org/schabi/newpipe/player/Player.java | 109 +++++++++++++++++- .../newpipe/player/helper/PlayerHelper.java | 50 ++++++++ app/src/main/res/layout/player.xml | 14 +++ app/src/main/res/values/strings.xml | 5 + 4 files changed, 177 insertions(+), 1 deletion(-) 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