From 9a1e6ca178d9833ee2c681fb130b9290a4e89cd8 Mon Sep 17 00:00:00 2001 From: MarcaD <152095496+MarcaDian@users.noreply.github.com> Date: Sun, 1 Jun 2025 12:12:56 +0300 Subject: [PATCH] feat(YouTube - Playback Speed): Use modern custom speed dialog (#5069) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../shared/spoof/requests/PlayerRoutes.java | 4 +- .../extension/youtube/ThemeHelper.java | 8 + .../patches/AlternativeThumbnailsPatch.java | 2 +- .../patches/ReturnYouTubeDislikePatch.java | 11 + .../youtube/patches/VideoInformation.java | 19 + .../PlaybackSpeedMenuFilterPatch.java | 20 +- .../speed/CustomPlaybackSpeedPatch.java | 533 ++++++++++++++++-- .../speed/RememberPlaybackSpeedPatch.java | 8 +- .../patches/theme/SeekbarColorPatch.java | 22 +- .../extension/youtube/settings/Settings.java | 2 +- .../PlaybackSpeedDialogButton.java | 27 +- .../youtube/video/information/Fingerprints.kt | 10 + .../information/VideoInformationPatch.kt | 70 +++ .../speed/custom/CustomPlaybackSpeedPatch.kt | 107 +--- .../video/speed/custom/Fingerprints.kt | 21 - .../kotlin/app/revanced/util/BytecodeUtils.kt | 39 ++ .../resources/addresources/values/strings.xml | 3 +- 17 files changed, 697 insertions(+), 209 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java index f45e890d5..5179b3e5f 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java @@ -71,9 +71,7 @@ final class PlayerRoutes { return innerTubeBody.toString(); } - /** - * @noinspection SameParameterValue - */ + @SuppressWarnings("SameParameterValue") static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java index 0177c9cbc..d6594f20d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java @@ -105,6 +105,14 @@ public class ThemeHelper { return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor(); } + public static int getDialogBackgroundColor() { + final String colorName = isDarkTheme() + ? "yt_black1" + : "yt_white1"; + + return Utils.getColorFromString(colorName); + } + public static int getToolbarBackgroundColor() { final String colorName = isDarkTheme() ? "yt_black3" diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java index c670a79de..a4dd50fcc 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java @@ -686,7 +686,7 @@ public final class AlternativeThumbnailsPatch { ? "" : fullUrl.substring(imageExtensionEndIndex); } - /** @noinspection SameParameterValue */ + @SuppressWarnings("SameParameterValue") String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) { // Images could be upgraded to webp if they are not already, but this fails quite often, // especially for new videos uploaded in the last hour. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java index b43eb77ab..862847410 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java @@ -365,6 +365,11 @@ public class ReturnYouTubeDislikePatch { if (videoId.equals(lastPrefetchedVideoId)) { return; } + if (!Utils.isNetworkConnected()) { + Logger.printDebug(() -> "Cannot pre-fetch RYD, network is not connected"); + lastPrefetchedVideoId = null; + return; + } final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort(); // Shorts shelf in home and subscription feed causes player response hook to be called, @@ -419,6 +424,12 @@ public class ReturnYouTubeDislikePatch { } Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType); + if (!Utils.isNetworkConnected()) { + Logger.printDebug(() -> "Cannot fetch RYD, network is not connected"); + currentVideoData = null; + return; + } + ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId); // Pre-emptively set the data to short status. // Required to prevent Shorts data from being used on a minimized video in incognito mode. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java index 6df9a9095..99d8a5b6a 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java @@ -354,4 +354,23 @@ public final class VideoInformation { return videoTime >= videoLength && videoLength > 0; } + /** + * Overrides the current playback speed. + * Rest of the implementation added by patch. + */ + public static void overridePlaybackSpeed(float speedOverride) { + Logger.printDebug(() -> "Overriding playback speed to: " + speedOverride); + } + + /** + * Injection point. + * + * @param newlyLoadedPlaybackSpeed The current playback speed. + */ + public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) { + if (playbackSpeed != newlyLoadedPlaybackSpeed) { + Logger.printDebug(() -> "Video speed changed: " + newlyLoadedPlaybackSpeed); + playbackSpeed = newlyLoadedPlaybackSpeed; + } + } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java index d630ee9ed..531578a8b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java @@ -10,18 +10,11 @@ import app.revanced.extension.youtube.settings.Settings; */ public final class PlaybackSpeedMenuFilterPatch extends Filter { - /** - * Old litho based speed selection menu. - */ - public static volatile boolean isOldPlaybackSpeedMenuVisible; - /** * 0.05x speed selection menu. */ public static volatile boolean isPlaybackRateSelectorMenuVisible; - private final StringFilterGroup oldPlaybackMenuGroup; - public PlaybackSpeedMenuFilterPatch() { // 0.05x litho speed menu. var playbackRateSelectorGroup = new StringFilterGroup( @@ -29,22 +22,13 @@ public final class PlaybackSpeedMenuFilterPatch extends Filter { "playback_rate_selector_menu_sheet.eml-js" ); - // Old litho based speed menu. - oldPlaybackMenuGroup = new StringFilterGroup( - Settings.CUSTOM_SPEED_MENU, - "playback_speed_sheet_content.eml-js"); - - addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup); + addPathCallbacks(playbackRateSelectorGroup); } @Override boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - if (matchedGroup == oldPlaybackMenuGroup) { - isOldPlaybackSpeedMenuVisible = true; - } else { - isPlaybackRateSelectorMenuVisible = true; - } + isPlaybackRateSelectorMenuVisible = true; return false; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java index 9b6224106..4c509bccb 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -1,24 +1,57 @@ package app.revanced.extension.youtube.patches.playback.speed; import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.dipToPixels; +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.icu.text.NumberFormat; import android.support.v7.widget.RecyclerView; +import android.view.animation.Animation; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.GridLayout; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.function.Function; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.ThemeHelper; +import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch; import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; @SuppressWarnings("unused") public class CustomPlaybackSpeedPatch { /** - * Maximum playback speed, exclusive value. Custom speeds must be less than this value. + * Maximum playback speed, inclusive. Custom speeds must be this or less. *

* Going over 8x does not increase the actual playback speed any higher, * and the UI selector starts flickering and acting weird. @@ -26,6 +59,11 @@ public class CustomPlaybackSpeedPatch { */ public static final float PLAYBACK_SPEED_MAXIMUM = 8; + /** + * Scale used to convert user speed to {@link android.widget.ProgressBar#setProgress(int)}. + */ + private static final float PROGRESS_BAR_VALUE_SCALE = 100; + /** * Tap and hold speed. */ @@ -34,16 +72,28 @@ public class CustomPlaybackSpeedPatch { /** * Custom playback speeds. */ - public static float[] customPlaybackSpeeds; + public static final float[] customPlaybackSpeeds; /** - * The last time the old playback menu was forcefully called. + * Formats speeds to UI strings. */ - private static long lastTimeOldPlaybackMenuInvoked; + private static final NumberFormat speedFormatter = NumberFormat.getNumberInstance(); + + /** + * Weak reference to the currently open dialog. + */ + private static WeakReference

currentDialog = new WeakReference<>(null); + + /** + * Minimum and maximum custom playback speeds of {@link #customPlaybackSpeeds}. + */ + private static final float customPlaybackSpeedsMin, customPlaybackSpeedsMax; static { - final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get(); + // Cap at 2 decimals (rounds automatically). + speedFormatter.setMaximumFractionDigits(2); + final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get(); if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) { TAP_AND_HOLD_SPEED = holdSpeed; } else { @@ -51,7 +101,9 @@ public class CustomPlaybackSpeedPatch { TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.resetToDefault(); } - loadCustomSpeeds(); + customPlaybackSpeeds = loadCustomSpeeds(); + customPlaybackSpeedsMin = customPlaybackSpeeds[0]; + customPlaybackSpeedsMax = customPlaybackSpeeds[customPlaybackSpeeds.length - 1]; } /** @@ -65,37 +117,41 @@ public class CustomPlaybackSpeedPatch { Utils.showToastLong(str("revanced_custom_playback_speeds_invalid", PLAYBACK_SPEED_MAXIMUM)); } - private static void loadCustomSpeeds() { + private static float[] loadCustomSpeeds() { try { - String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+"); + // Automatically replace commas with periods, + // if the user added speeds in a localized format. + String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get() + .replace(',', '.').split("\\s+"); Arrays.sort(speedStrings); if (speedStrings.length == 0) { throw new IllegalArgumentException(); } - customPlaybackSpeeds = new float[speedStrings.length]; + float[] speeds = new float[speedStrings.length]; int i = 0; for (String speedString : speedStrings) { final float speedFloat = Float.parseFloat(speedString); - if (speedFloat <= 0 || arrayContains(customPlaybackSpeeds, speedFloat)) { + if (speedFloat <= 0 || arrayContains(speeds, speedFloat)) { throw new IllegalArgumentException(); } - if (speedFloat >= PLAYBACK_SPEED_MAXIMUM) { + if (speedFloat > PLAYBACK_SPEED_MAXIMUM) { showInvalidCustomSpeedToast(); Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault(); - loadCustomSpeeds(); - return; + return loadCustomSpeeds(); } - customPlaybackSpeeds[i++] = speedFloat; + speeds[i++] = speedFloat; } + + return speeds; } catch (Exception ex) { - Logger.printInfo(() -> "parse error", ex); - Utils.showToastLong(str("revanced_custom_playback_speeds_parse_exception")); + Logger.printInfo(() -> "Parse error", ex); + Utils.showToastShort(str("revanced_custom_playback_speeds_parse_exception")); Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault(); - loadCustomSpeeds(); + return loadCustomSpeeds(); } } @@ -113,38 +169,28 @@ public class CustomPlaybackSpeedPatch { recyclerView.getViewTreeObserver().addOnDrawListener(() -> { try { if (PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible) { - if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) { + if (hideLithoMenuAndShowCustomSpeedMenu(recyclerView, 5)) { PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible = false; } - return; } } catch (Exception ex) { - Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex); - } - - try { - if (PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible) { - if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) { - PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible = false; - } - } - } catch (Exception ex) { - Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex); + Logger.printException(() -> "onFlyoutMenuCreate failure", ex); } }); } - private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) { + @SuppressWarnings("SameParameterValue") + private static boolean hideLithoMenuAndShowCustomSpeedMenu(RecyclerView recyclerView, int expectedChildCount) { if (recyclerView.getChildCount() == 0) { return false; } View firstChild = recyclerView.getChildAt(0); - if (!(firstChild instanceof ViewGroup PlaybackSpeedParentView)) { + if (!(firstChild instanceof ViewGroup playbackSpeedParentView)) { return false; } - if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) { + if (playbackSpeedParentView.getChildCount() != expectedChildCount) { return false; } @@ -168,23 +214,418 @@ public class CustomPlaybackSpeedPatch { ((ViewGroup) parentView3rd).setVisibility(View.GONE); ((ViewGroup) parentView4th).setVisibility(View.GONE); - // Close the litho speed menu and show the old one. - showOldPlaybackSpeedMenu(); + // Close the litho speed menu and show the modern custom speed dialog. + showModernCustomPlaybackSpeedDialog(recyclerView.getContext()); + Logger.printDebug(() -> "Modern playback speed dialog shown"); return true; } - public static void showOldPlaybackSpeedMenu() { - // This method is sometimes used multiple times. - // To prevent this, ignore method reuse within 1 second. - final long now = System.currentTimeMillis(); - if (now - lastTimeOldPlaybackMenuInvoked < 1000) { - Logger.printDebug(() -> "Ignoring call to showOldPlaybackSpeedMenu"); - return; - } - lastTimeOldPlaybackMenuInvoked = now; - Logger.printDebug(() -> "Old video quality menu shown"); + /** + * Displays a modern custom dialog for adjusting video playback speed. + *

+ * This method creates a dialog with a slider, plus/minus buttons, and preset speed buttons + * to allow the user to modify the video playback speed. The dialog is styled with rounded + * corners and themed colors, positioned at the bottom of the screen. The playback speed + * can be adjusted in 0.05 increments using the slider or buttons, or set directly to preset + * values. The dialog updates the displayed speed in real-time and applies changes to the + * video playback. The dialog is dismissed if the player enters Picture-in-Picture (PiP) mode. + */ + @SuppressLint("SetTextI18n") + public static void showModernCustomPlaybackSpeedDialog(Context context) { + // Create a dialog without a theme for custom appearance. + Dialog dialog = new Dialog(context); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar. - // Rest of the implementation added by patch. + // Store the dialog reference. + currentDialog = new WeakReference<>(dialog); + + // Create main vertical LinearLayout for dialog content. + LinearLayout mainLayout = new LinearLayout(context); + mainLayout.setOrientation(LinearLayout.VERTICAL); + + // Preset size constants. + final int dip4 = dipToPixels(4); // Height for handle bar. + final int dip5 = dipToPixels(5); + final int dip6 = dipToPixels(6); // Padding for mainLayout from bottom. + final int dip8 = dipToPixels(8); // Padding for mainLayout from left and right. + final int dip20 = dipToPixels(20); + final int dip32 = dipToPixels(32); // Height for in-rows speed buttons. + final int dip36 = dipToPixels(36); // Height for minus and plus buttons. + final int dip40 = dipToPixels(40); // Width for handle bar. + final int dip60 = dipToPixels(60); // Height for speed button container. + + mainLayout.setPadding(dip5, dip8, dip5, dip8); + + // Set rounded rectangle background for the main layout. + RoundRectShape roundRectShape = new RoundRectShape( + createCornerRadii(12), null, null); + ShapeDrawable background = new ShapeDrawable(roundRectShape); + background.getPaint().setColor(ThemeHelper.getDialogBackgroundColor()); + mainLayout.setBackground(background); + + // Add handle bar at the top. + View handleBar = new View(context); + ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape( + createCornerRadii(4), null, null)); + handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true)); + handleBar.setBackground(handleBackground); + LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams( + dip40, // handle bar width. + dip4 // handle bar height. + ); + handleParams.gravity = Gravity.CENTER_HORIZONTAL; // Center horizontally. + handleParams.setMargins(0, 0, 0, dip20); // 20dp bottom margins. + handleBar.setLayoutParams(handleParams); + // Add handle bar view to main layout. + mainLayout.addView(handleBar); + + // Display current playback speed. + TextView currentSpeedText = new TextView(context); + float currentSpeed = VideoInformation.getPlaybackSpeed(); + // Initially show with only 0 minimum digits, so 1.0 shows as 1x + currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0)); + currentSpeedText.setTextColor(ThemeHelper.getForegroundColor()); + currentSpeedText.setTextSize(16); + currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD); + currentSpeedText.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + textParams.setMargins(0, 0, 0, 0); + currentSpeedText.setLayoutParams(textParams); + // Add current speed text view to main layout. + mainLayout.addView(currentSpeedText); + + // Create horizontal layout for slider and +/- buttons. + LinearLayout sliderLayout = new LinearLayout(context); + sliderLayout.setOrientation(LinearLayout.HORIZONTAL); + sliderLayout.setGravity(Gravity.CENTER_VERTICAL); + sliderLayout.setPadding(dip5, dip5, dip5, dip5); // 5dp padding. + + // Create minus button. + Button minusButton = new Button(context, null, 0); // Disable default theme style. + minusButton.setText(""); // No text on button. + ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null)); + minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false)); + minusButton.setBackground(minusBackground); + OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol. + minusButton.setForeground(minusDrawable); + LinearLayout.LayoutParams minusParams = new LinearLayout.LayoutParams(dip36, dip36); + minusParams.setMargins(0, 0, dip5, 0); // 5dp to slider. + minusButton.setLayoutParams(minusParams); + + // Create slider for speed adjustment. + SeekBar speedSlider = new SeekBar(context); + speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax)); + speedSlider.setProgress(speedToProgressValue(currentSpeed)); + speedSlider.getProgressDrawable().setColorFilter( + ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar. + speedSlider.getThumb().setColorFilter( + ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb. + LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); + sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons. + speedSlider.setLayoutParams(sliderParams); + + // Create plus button. + Button plusButton = new Button(context, null, 0); // Disable default theme style. + plusButton.setText(""); // No text on button. + ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape( + createCornerRadii(20), null, null)); + plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false)); + plusButton.setBackground(plusBackground); + OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol. + plusButton.setForeground(plusDrawable); + LinearLayout.LayoutParams plusParams = new LinearLayout.LayoutParams(dip36, dip36); + plusParams.setMargins(dip5, 0, 0, 0); // 5dp to slider. + plusButton.setLayoutParams(plusParams); + + // Add -/+ and slider views to slider layout. + sliderLayout.addView(minusButton); + sliderLayout.addView(speedSlider); + sliderLayout.addView(plusButton); + + LinearLayout.LayoutParams sliderLayoutParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + sliderLayoutParams.setMargins(0, 0, 0, dip5); // 5dp bottom margin. + sliderLayout.setLayoutParams(sliderLayoutParams); + + // Add slider layout to main layout. + mainLayout.addView(sliderLayout); + + Function userSelectedSpeed = newSpeed -> { + final float roundedSpeed = roundSpeedToNearestIncrement(newSpeed); + if (VideoInformation.getPlaybackSpeed() == roundedSpeed) { + // Nothing has changed. New speed rounds to the current speed. + return null; + } + + VideoInformation.overridePlaybackSpeed(roundedSpeed); + RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed); + currentSpeedText.setText(formatSpeedStringX(roundedSpeed, 2)); // Update display. + speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider. + return null; + }; + + // Set listener for slider to update playback speed. + speedSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + // Convert from progress value to video playback speed. + userSelectedSpeed.apply(customPlaybackSpeedsMin + (progress / PROGRESS_BAR_VALUE_SCALE)); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(SeekBar seekBar) {} + }); + + minusButton.setOnClickListener(v -> userSelectedSpeed.apply( + VideoInformation.getPlaybackSpeed() - 0.05f)); + plusButton.setOnClickListener(v -> userSelectedSpeed.apply( + VideoInformation.getPlaybackSpeed() + 0.05f)); + + // Create GridLayout for preset speed buttons. + GridLayout gridLayout = new GridLayout(context); + gridLayout.setColumnCount(5); // 5 columns for speed buttons. + gridLayout.setAlignmentMode(GridLayout.ALIGN_BOUNDS); + gridLayout.setRowCount((int) Math.ceil(customPlaybackSpeeds.length / 5.0)); + LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + gridParams.setMargins(0, 0, 0, 0); // No margins around GridLayout. + gridLayout.setLayoutParams(gridParams); + + // For all buttons show at least 1 zero in decimal (2 -> "2.0"). + speedFormatter.setMinimumFractionDigits(1); + + // Add buttons for each preset playback speed. + for (float speed : customPlaybackSpeeds) { + // Container for button and optional label. + FrameLayout buttonContainer = new FrameLayout(context); + + // Set layout parameters for each grid cell. + GridLayout.LayoutParams containerParams = new GridLayout.LayoutParams(); + containerParams.width = 0; // Equal width for columns. + containerParams.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1, 1f); + containerParams.setMargins(dip5, 0, dip5, 0); // Button margins. + containerParams.height = dip60; // Fixed height for button and label. + buttonContainer.setLayoutParams(containerParams); + + // Create speed button. + Button speedButton = new Button(context, null, 0); + speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format. + speedButton.setTextColor(ThemeHelper.getForegroundColor()); + speedButton.setTextSize(12); + speedButton.setAllCaps(false); + speedButton.setGravity(Gravity.CENTER); + + ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape( + createCornerRadii(20), null, null)); + buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false)); + speedButton.setBackground(buttonBackground); + speedButton.setPadding(dip5, dip5, dip5, dip5); + + // Center button vertically and stretch horizontally in container. + FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, dip32, Gravity.CENTER); + speedButton.setLayoutParams(buttonParams); + + // Add speed buttons view to buttons container layout. + buttonContainer.addView(speedButton); + + // Add "Normal" label for 1.0x speed. + if (speed == 1.0f) { + TextView normalLabel = new TextView(context); + // Use same 'Normal' string as stock YouTube. + normalLabel.setText(str("normal_playback_rate_label")); + normalLabel.setTextColor(ThemeHelper.getForegroundColor()); + normalLabel.setTextSize(10); + normalLabel.setGravity(Gravity.CENTER); + + FrameLayout.LayoutParams labelParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + labelParams.bottomMargin = 0; // Position label below button. + normalLabel.setLayoutParams(labelParams); + + buttonContainer.addView(normalLabel); + } + + speedButton.setOnClickListener(v -> userSelectedSpeed.apply(speed)); + + gridLayout.addView(buttonContainer); + } + + // Add in-rows speed buttons layout to main layout. + mainLayout.addView(gridLayout); + + // Wrap mainLayout in another LinearLayout for side margins. + LinearLayout wrapperLayout = new LinearLayout(context); + wrapperLayout.setOrientation(LinearLayout.VERTICAL); + wrapperLayout.setPadding(dip8, 0, dip8, 0); // 8dp side margins. + wrapperLayout.addView(mainLayout); + dialog.setContentView(wrapperLayout); + + // Configure dialog window to appear at the bottom. + Window window = dialog.getWindow(); + if (window != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.gravity = Gravity.BOTTOM; // Position at bottom of screen. + params.y = dip6; // 6dp margin from bottom. + // In landscape, use the smaller dimension (height) as portrait width. + int portraitWidth = context.getResources().getDisplayMetrics().widthPixels; + if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + portraitWidth = Math.min( + portraitWidth, + context.getResources().getDisplayMetrics().heightPixels); + } + params.width = portraitWidth; // Use portrait width. + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(params); + window.setBackgroundDrawable(null); // Remove default dialog background. + } + + // Create observer for PlayerType changes. + Function1 playerTypeObserver = new Function1<>() { + @Override + public Unit invoke(PlayerType type) { + Dialog current = currentDialog.get(); + if (current == null || !current.isShowing()) { + // Should never happen. + PlayerType.getOnChange().removeObserver(this); + Logger.printException(() -> "Removing player type listener as dialog is null or closed"); + } else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) { + current.dismiss(); + Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode"); + } + return Unit.INSTANCE; + } + }; + + // Add observer to dismiss dialog when entering PiP mode. + PlayerType.getOnChange().addObserver(playerTypeObserver); + + // Remove observer when dialog is dismissed. + dialog.setOnDismissListener(d -> { + PlayerType.getOnChange().removeObserver(playerTypeObserver); + Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss"); + }); + + // Apply slide-in animation when showing the dialog. + final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast"); + Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom"); + slideInABottomAnimation.setDuration(fadeDurationFast); + mainLayout.startAnimation(slideInABottomAnimation); + + dialog.show(); // Display the dialog. + } + + /** + * Creates an array of corner radii for a rounded rectangle shape. + * + * @param dp The radius in density-independent pixels (dp) to apply to all corners. + * @return An array of eight float values representing the corner radii + * (top-left, top-right, bottom-right, bottom-left). + */ + private static float[] createCornerRadii(float dp) { + final float radius = dipToPixels(dp); + return new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; + } + + /** + * @param speed The playback speed value to format. + * @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x"). + */ + private static String formatSpeedStringX(float speed, int minimumFractionDigits) { + speedFormatter.setMinimumFractionDigits(minimumFractionDigits); + return speedFormatter.format(speed) + 'x'; + } + + /** + * @return user speed converted to a value for {@link SeekBar#setProgress(int)}. + */ + private static int speedToProgressValue(float speed) { + return (int) ((speed - customPlaybackSpeedsMin) * PROGRESS_BAR_VALUE_SCALE); + } + + /** + * Rounds the given playback speed to the nearest 0.05 increment and ensures it is within valid bounds. + * + * @param speed The playback speed to round. + * @return The rounded speed, constrained to the specified bounds. + */ + private static float roundSpeedToNearestIncrement(float speed) { + // Round to nearest 0.05 speed. + final float roundedSpeed = Math.round(speed / 0.05f) * 0.05f; + return Utils.clamp(roundedSpeed, 0.05f, PLAYBACK_SPEED_MAXIMUM); + } + + /** + * Adjusts the background color based on the current theme. + * + * @param isHandleBar If true, applies a stronger darkening factor (0.9) for the handle bar in light theme; + * if false, applies a standard darkening factor (0.95) for other elements in light theme. + * @return A modified background color, lightened by 20% for dark themes or darkened by 5% (or 10% for handle bar) + * for light themes to ensure visual contrast. + */ + public static int getAdjustedBackgroundColor(boolean isHandleBar) { + final int baseColor = ThemeHelper.getDialogBackgroundColor(); + float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme. + float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme. + return ThemeHelper.isDarkTheme() + ? ThemeHelper.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme. + : ThemeHelper.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme. + } +} + +/** + * Custom Drawable for rendering outlined plus and minus symbols on buttons. + */ +class OutlineSymbolDrawable extends Drawable { + private final boolean isPlus; // Determines if the symbol is a plus or minus. + private final Paint paint; + + OutlineSymbolDrawable(boolean isPlus) { + this.isPlus = isPlus; + paint = new Paint(Paint.ANTI_ALIAS_FLAG); // Enable anti-aliasing for smooth rendering. + paint.setColor(ThemeHelper.getForegroundColor()); + paint.setStyle(Paint.Style.STROKE); // Use stroke style for outline. + paint.setStrokeWidth(dipToPixels(1)); // 1dp stroke width. + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + final int width = bounds.width(); + final int height = bounds.height(); + final float centerX = width / 2f; // Center X coordinate. + final float centerY = height / 2f; // Center Y coordinate. + final float size = Math.min(width, height) * 0.25f; // Symbol size is 25% of button dimensions. + + // Draw horizontal line for both plus and minus symbols. + canvas.drawLine(centerX - size, centerY, centerX + size, centerY, paint); + if (isPlus) { + // Draw vertical line for plus symbol. + canvas.drawLine(centerX, centerY - size, centerX, centerY + size, paint); + } + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java index a6c86477c..04840c761 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -33,10 +33,10 @@ public final class RememberPlaybackSpeedPatch { public static void userSelectedPlaybackSpeed(float playbackSpeed) { try { if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) { - // With the 0.05x menu, if the speed is set by integrations to higher than 2.0x + // With the 0.05x menu, if the speed is set by a patch to higher than 2.0x // then the menu will allow increasing without bounds but the max speed is - // still capped to under 8.0x. - playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f); + // still capped to 8.0x. + playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM); // Prevent toast spamming if using the 0.05x adjustments. // Show exactly one toast after the user stops interacting with the speed menu. @@ -57,7 +57,7 @@ public final class RememberPlaybackSpeedPatch { } Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed); - Utils.showToastLong(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x"))); + Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x"))); }, TOAST_DELAY_MILLISECONDS); } } catch (Exception ex) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java index 1489ffd51..2156bc693 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java @@ -60,7 +60,7 @@ public final class SeekbarColorPatch { * this is the color value of {@link Settings#SEEKBAR_CUSTOM_COLOR_PRIMARY}. * Otherwise this is {@link #ORIGINAL_SEEKBAR_COLOR}. */ - private static int customSeekbarColor = ORIGINAL_SEEKBAR_COLOR; + private static final int customSeekbarColor; /** * Custom seekbar hue, saturation, and brightness values. @@ -77,24 +77,25 @@ public final class SeekbarColorPatch { Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv); ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2]; - if (SEEKBAR_CUSTOM_COLOR_ENABLED) { - loadCustomSeekbarColor(); - } + customSeekbarColor = SEEKBAR_CUSTOM_COLOR_ENABLED + ? loadCustomSeekbarColor() + : ORIGINAL_SEEKBAR_COLOR; } - private static void loadCustomSeekbarColor() { + private static int loadCustomSeekbarColor() { try { - customSeekbarColor = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.get()); - Color.colorToHSV(customSeekbarColor, customSeekbarColorHSV); - - customSeekbarColorGradient[0] = customSeekbarColor; + final int color = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.get()); + Color.colorToHSV(color, customSeekbarColorHSV); + customSeekbarColorGradient[0] = color; customSeekbarColorGradient[1] = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.get()); + + return color; } catch (Exception ex) { Utils.showToastShort(str("revanced_seekbar_custom_color_invalid")); Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.resetToDefault(); Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.resetToDefault(); - loadCustomSeekbarColor(); + return loadCustomSeekbarColor(); } } @@ -114,6 +115,7 @@ public final class SeekbarColorPatch { : (int) channel3Bits; } + @SuppressWarnings("SameParameterValue") private static String get9BitStyleIdentifier(int color24Bit) { final int r3 = colorChannelTo3Bits(Color.red(color24Bit)); final int g3 = colorChannelTo3Bits(Color.green(color24Bit)); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index 92b222161..4613a5a14 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -63,7 +63,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting CUSTOM_SPEED_MENU = new BooleanSetting("revanced_custom_speed_menu", TRUE); public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", -2.0f); public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", - "0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true); + "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.5\n3.0\n4.0\n5.0\n6.0\n7.0\n8.0", true); // Audio public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, new ForceOriginalAudioAvailability()); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton.java index 3ac387061..a61ed679c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton.java @@ -5,9 +5,13 @@ import android.view.View; import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; import app.revanced.extension.youtube.settings.Settings; +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.showToastShort; + @SuppressWarnings("unused") public class PlaybackSpeedDialogButton { @Nullable @@ -23,8 +27,27 @@ public class PlaybackSpeedDialogButton { "revanced_playback_speed_dialog_button", "revanced_playback_speed_dialog_button_placeholder", Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get, - view -> CustomPlaybackSpeedPatch.showOldPlaybackSpeedMenu(), - null + view -> { + try { + CustomPlaybackSpeedPatch.showModernCustomPlaybackSpeedDialog(view.getContext()); + } catch (Exception ex) { + Logger.printException(() -> "speed button onClick failure", ex); + } + }, + view -> { + try { + final float speed = (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() || + VideoInformation.getPlaybackSpeed() == Settings.PLAYBACK_SPEED_DEFAULT.get()) + ? 1.0f + : Settings.PLAYBACK_SPEED_DEFAULT.get(); + + VideoInformation.overridePlaybackSpeed(speed); + showToastShort(str("revanced_custom_playback_speeds_reset_toast", (speed + "x"))); + } catch (Exception ex) { + Logger.printException(() -> "speed button reset failure", ex); + } + return true; + } ); } catch (Exception ex) { Logger.printException(() -> "initializeButton failure", ex); diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt index 349f7300b..5157f9823 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt @@ -123,3 +123,13 @@ internal val playbackSpeedMenuSpeedChangedFingerprint = fingerprint { Opcode.RETURN_OBJECT, ) } + +internal val playbackSpeedClassFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + parameters("L") + opcodes( + Opcode.RETURN_OBJECT + ) + strings("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt index a22934d1c..5a9c5e740 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt @@ -7,6 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableClass import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.util.smali.toInstructions import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint import app.revanced.patches.youtube.video.playerresponse.Hook @@ -16,6 +17,8 @@ import app.revanced.patches.youtube.video.videoid.hookBackgroundPlayVideoId import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.revanced.patches.youtube.video.videoid.hookVideoId import app.revanced.patches.youtube.video.videoid.videoIdPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.addStaticFieldToExtension import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags @@ -29,6 +32,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter import com.android.tools.smali.dexlib2.util.MethodUtil @@ -189,6 +193,72 @@ val videoInformationPatch = bytecodePatch( proxy(classes.first { it.type == setPlaybackSpeedMethodReference.definingClass }) .mutableClass.methods.first { it.name == setPlaybackSpeedMethodReference.name } setPlaybackSpeedMethodIndex = 0 + + // Add override playback speed method. + onPlaybackSpeedItemClickFingerprint.classDef.methods.add( + ImmutableMethod( + definingClass, + "overridePlaybackSpeed", + listOf(ImmutableMethodParameter("F", annotations, null)), + "V", + AccessFlags.PUBLIC.value or AccessFlags.PUBLIC.value, + annotations, + null, + ImmutableMethodImplementation( + 4, + """ + # Check if the playback speed is not auto (-2.0f) + const/4 v0, 0x0 + cmpg-float v0, v3, v0 + if-lez v0, :ignore + + # Get the container class field. + iget-object v0, v2, $setPlaybackSpeedContainerClassFieldReference + + # For some reason, in YouTube 19.44.39 this value is sometimes null. + if-eqz v0, :ignore + + # Get the field from its class. + iget-object v1, v0, $setPlaybackSpeedClassFieldReference + + # Invoke setPlaybackSpeed on that class. + invoke-virtual {v1, v3}, $setPlaybackSpeedMethodReference + + :ignore + return-void + """.toInstructions(), null, null + ) + ).toMutable() + ) + } + + playbackSpeedClassFingerprint.method.apply { + val index = indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT) + val register = getInstruction(index).registerA + val playbackSpeedClass = this.returnType + + // Set playback speed class. + addInstructionsAtControlFlowLabel( + index, + "sput-object v$register, $EXTENSION_CLASS_DESCRIPTOR->playbackSpeedClass:$playbackSpeedClass" + ) + + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0, p0}, $playbackSpeedClass->overridePlaybackSpeed(F)V + return-void + :ignore + nop + """ + + addStaticFieldToExtension( + EXTENSION_CLASS_DESCRIPTOR, + "overridePlaybackSpeed", + "playbackSpeedClass", + playbackSpeedClass, + smaliInstructions + ) } // Handle new playback speed menu. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt index d6a7fb561..027be3a1b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt @@ -1,19 +1,11 @@ package app.revanced.patches.youtube.video.speed.custom -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.patch.resourcePatch -import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.mapping.get -import app.revanced.patches.shared.misc.mapping.resourceMappingPatch -import app.revanced.patches.shared.misc.mapping.resourceMappings import app.revanced.patches.shared.misc.settings.preference.InputType import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.settings.preference.TextPreference @@ -27,27 +19,11 @@ import app.revanced.patches.youtube.misc.recyclerviewtree.hook.addRecyclerViewTr import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.video.speed.settingsMenuVideoSpeedGroup -import app.revanced.util.* -import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstruction +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.immutable.ImmutableField - -internal var speedUnavailableId = -1L - private set - -private val customPlaybackSpeedResourcePatch = resourcePatch { - dependsOn(resourceMappingPatch) - - execute { - speedUnavailableId = resourceMappings[ - "string", - "varispeed_unavailable_message", - ] - } -} private const val FILTER_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch;" @@ -64,8 +40,7 @@ internal val customPlaybackSpeedPatch = bytecodePatch( addResourcesPatch, lithoFilterPatch, versionCheckPatch, - recyclerViewTreeHookPatch, - customPlaybackSpeedResourcePatch + recyclerViewTreeHookPatch ) execute { @@ -87,38 +62,6 @@ internal val customPlaybackSpeedPatch = bytecodePatch( ) } - // Replace the speeds float array with custom speeds. - speedArrayGeneratorFingerprint.method.apply { - val sizeCallIndex = indexOfFirstInstructionOrThrow { getReference()?.name == "size" } - val sizeCallResultRegister = getInstruction(sizeCallIndex + 1).registerA - - replaceInstruction(sizeCallIndex + 1, "const/4 v$sizeCallResultRegister, 0x0") - - val arrayLengthConstIndex = indexOfFirstLiteralInstructionOrThrow(7) - val arrayLengthConstDestination = getInstruction(arrayLengthConstIndex).registerA - val playbackSpeedsArrayType = "$EXTENSION_CLASS_DESCRIPTOR->customPlaybackSpeeds:[F" - - addInstructions( - arrayLengthConstIndex + 1, - """ - sget-object v$arrayLengthConstDestination, $playbackSpeedsArrayType - array-length v$arrayLengthConstDestination, v$arrayLengthConstDestination - """, - ) - - val originalArrayFetchIndex = indexOfFirstInstructionOrThrow { - val reference = getReference() - reference?.type == "[F" && reference.definingClass.endsWith("/PlayerConfigModel;") - } - val originalArrayFetchDestination = - getInstruction(originalArrayFetchIndex).registerA - - replaceInstruction( - originalArrayFetchIndex, - "sget-object v$originalArrayFetchDestination, $playbackSpeedsArrayType", - ) - } - // Override the min/max speeds that can be used. speedLimiterFingerprint.method.apply { val limitMinIndex = indexOfFirstLiteralInstructionOrThrow(0.25f) @@ -135,47 +78,7 @@ internal val customPlaybackSpeedPatch = bytecodePatch( replaceInstruction(limitMaxIndex, "const/high16 v$limitMaxRegister, 8.0f") } - // Add a static INSTANCE field to the class. - // This is later used to call "showOldPlaybackSpeedMenu" on the instance. - - val instanceField = ImmutableField( - getOldPlaybackSpeedsFingerprint.originalClassDef.type, - "INSTANCE", - getOldPlaybackSpeedsFingerprint.originalClassDef.type, - AccessFlags.PUBLIC.value or AccessFlags.STATIC.value, - null, - null, - null, - ).toMutable() - - getOldPlaybackSpeedsFingerprint.classDef.staticFields.add(instanceField) - // Set the INSTANCE field to the instance of the class. - // In order to prevent a conflict with another patch, add the instruction at index 1. - getOldPlaybackSpeedsFingerprint.method.addInstruction(1, "sput-object p0, $instanceField") - - // Get the "showOldPlaybackSpeedMenu" method. - // This is later called on the field INSTANCE. - val showOldPlaybackSpeedMenuMethod = showOldPlaybackSpeedMenuFingerprint.match( - getOldPlaybackSpeedsFingerprint.classDef, - ).method.toString() - - // Insert the call to the "showOldPlaybackSpeedMenu" method on the field INSTANCE. - showOldPlaybackSpeedMenuExtensionFingerprint.method.apply { - addInstructionsWithLabels( - instructions.lastIndex, - """ - sget-object v0, $instanceField - if-nez v0, :not_null - return-void - :not_null - invoke-virtual { v0 }, $showOldPlaybackSpeedMenuMethod - """, - ) - } - - // region Force old video quality menu. - // This is necessary, because there is no known way of adding custom playback speeds to the new menu. - + // Close the unpatched playback dialog and show the modern custom dialog. addRecyclerViewTreeHook(EXTENSION_CLASS_DESCRIPTOR) // Required to check if the playback speed menu is currently shown. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt index 7d4b23ba4..120bb1df3 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt @@ -1,30 +1,9 @@ package app.revanced.patches.youtube.video.speed.custom import app.revanced.patcher.fingerprint -import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -internal val getOldPlaybackSpeedsFingerprint = fingerprint { - parameters("[L", "I") - strings("menu_item_playback_speed") -} - -internal val showOldPlaybackSpeedMenuFingerprint = fingerprint { - literal { speedUnavailableId } -} - -internal val showOldPlaybackSpeedMenuExtensionFingerprint = fingerprint { - custom { method, _ -> method.name == "showOldPlaybackSpeedMenu" } -} - -internal val speedArrayGeneratorFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) - returns("[L") - parameters("Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;") - strings("0.0#") -} - internal val speedLimiterFingerprint = fingerprint { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returns("V") diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 7e8c5ab21..afcae5f71 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -10,6 +10,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.PatchException import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.smali.ExternalLabel import app.revanced.patches.shared.misc.mapping.get @@ -31,6 +32,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstructio import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction import com.android.tools.smali.dexlib2.iface.reference.Reference +import com.android.tools.smali.dexlib2.immutable.ImmutableField import com.android.tools.smali.dexlib2.util.MethodUtil import java.util.EnumSet @@ -962,6 +964,43 @@ private fun MutableMethod.overrideReturnValue(value: String, returnLate: Boolean } } +internal fun BytecodePatchContext.addStaticFieldToExtension( + className: String, + methodName: String, + fieldName: String, + objectClass: String, + smaliInstructions: String +) { + val classDef = classes.find { classDef -> classDef.type == className } + ?: throw PatchException("No matching methods found in: $className") + val mutableClass = proxy(classDef).mutableClass + + val objectCall = "$mutableClass->$fieldName:$objectClass" + + mutableClass.apply { + methods.first { method -> method.name == methodName }.apply { + staticFields.add( + ImmutableField( + definingClass, + fieldName, + objectClass, + AccessFlags.PUBLIC.value or AccessFlags.STATIC.value, + null, + annotations, + null + ).toMutable() + ) + + addInstructionsWithLabels( + 0, + """ + sget-object v0, $objectCall + """ + smaliInstructions + ) + } + } +} + /** * Set the custom condition for this fingerprint to check for a literal value. * diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 0a51eedb0..5e371ecf8 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1466,7 +1466,7 @@ Enabling this can unlock higher video qualities" Show speed dialog button - Button is shown + Button is shown. Tap and hold to reset playback speed to default Button is not shown @@ -1478,6 +1478,7 @@ Enabling this can unlock higher video qualities" Custom speeds must be less than %s Invalid custom playback speeds Auto + Playback speed reset to: %s Custom tap and hold speed Playback speed between 0-8