diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java index e49ff08532..d630ee9ed8 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java @@ -3,25 +3,48 @@ import androidx.annotation.Nullable; import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; +import app.revanced.extension.youtube.settings.Settings; /** * Abuse LithoFilter for {@link CustomPlaybackSpeedPatch}. */ public final class PlaybackSpeedMenuFilterPatch extends Filter { - // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. - public static volatile boolean isPlaybackSpeedMenuVisible; + + /** + * 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() { - addPathCallbacks(new StringFilterGroup( - null, - "playback_speed_sheet_content.eml-js" - )); + // 0.05x litho speed menu. + var playbackRateSelectorGroup = new StringFilterGroup( + Settings.CUSTOM_SPEED_MENU, + "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); } @Override boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - isPlaybackSpeedMenuVisible = true; + if (matchedGroup == oldPlaybackMenuGroup) { + isOldPlaybackSpeedMenuVisible = true; + } else { + isPlaybackRateSelectorMenuVisible = true; + } return false; } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java index cad6050fba..d015c192a0 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -59,18 +59,24 @@ private static void loadCustomSpeeds() { if (speedStrings.length == 0) { throw new IllegalArgumentException(); } + customPlaybackSpeeds = new float[speedStrings.length]; - for (int i = 0, length = speedStrings.length; i < length; i++) { - final float speed = Float.parseFloat(speedStrings[i]); - if (speed <= 0 || arrayContains(customPlaybackSpeeds, speed)) { + + int i = 0; + for (String speedString : speedStrings) { + final float speedFloat = Float.parseFloat(speedString); + if (speedFloat <= 0 || arrayContains(customPlaybackSpeeds, speedFloat)) { throw new IllegalArgumentException(); } - if (speed >= MAXIMUM_PLAYBACK_SPEED) { + + if (speedFloat >= MAXIMUM_PLAYBACK_SPEED) { resetCustomSpeeds(str("revanced_custom_playback_speeds_invalid", MAXIMUM_PLAYBACK_SPEED)); loadCustomSpeeds(); return; } - customPlaybackSpeeds[i] = speed; + + customPlaybackSpeeds[i] = speedFloat; + i++; } } catch (Exception ex) { Logger.printInfo(() -> "parse error", ex); @@ -89,10 +95,12 @@ private static boolean arrayContains(float[] array, float value) { /** * Initialize a settings preference list with the available playback speeds. */ + @SuppressWarnings("deprecation") public static void initializeListPreference(ListPreference preference) { if (preferenceListEntries == null) { preferenceListEntries = new String[customPlaybackSpeeds.length]; preferenceListEntryValues = new String[customPlaybackSpeeds.length]; + int i = 0; for (float speed : customPlaybackSpeeds) { String speedString = String.valueOf(speed); @@ -101,6 +109,7 @@ public static void initializeListPreference(ListPreference preference) { i++; } } + preference.setEntries(preferenceListEntries); preference.setEntryValues(preferenceListEntryValues); } @@ -111,52 +120,67 @@ public static void initializeListPreference(ListPreference preference) { public static void onFlyoutMenuCreate(RecyclerView recyclerView) { recyclerView.getViewTreeObserver().addOnDrawListener(() -> { try { - // For some reason, the custom playback speed flyout panel is activated when the user opens the share panel. (A/B tests) - // Check the child count of playback speed flyout panel to prevent this issue. - // Child count of playback speed flyout panel is always 8. - if (!PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible || recyclerView.getChildCount() == 0) { + if (PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible) { + if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) { + PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible = false; + } return; } + } catch (Exception ex) { + Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex); + } - View firstChild = recyclerView.getChildAt(0); - if (!(firstChild instanceof ViewGroup)) { - return; - } - ViewGroup PlaybackSpeedParentView = (ViewGroup) firstChild; - if (PlaybackSpeedParentView.getChildCount() != 8) { - return; + try { + if (PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible) { + if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) { + PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible = false; + } } + } catch (Exception ex) { + Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex); + } + }); + } - PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false; + private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) { + if (recyclerView.getChildCount() == 0) { + return false; + } - ViewParent parentView3rd = Utils.getParentView(recyclerView, 3); - if (!(parentView3rd instanceof ViewGroup)) { - return; - } - ViewParent parentView4th = parentView3rd.getParent(); - if (!(parentView4th instanceof ViewGroup)) { - return; - } + View firstChild = recyclerView.getChildAt(0); + if (!(firstChild instanceof ViewGroup)) { + return false; + } + + ViewGroup PlaybackSpeedParentView = (ViewGroup) firstChild; + if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) { + return false; + } - // Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView. - // This only shows in phone layout. - final var touchInsidedView = ((ViewGroup) parentView4th).getChildAt(0); - touchInsidedView.setSoundEffectsEnabled(false); - touchInsidedView.performClick(); + ViewParent parentView3rd = Utils.getParentView(recyclerView, 3); + if (!(parentView3rd instanceof ViewGroup)) { + return true; + } - // In tablet layout there is no Dismiss View, instead we just hide all two parent views. - ((ViewGroup) parentView3rd).setVisibility(View.GONE); - ((ViewGroup) parentView4th).setVisibility(View.GONE); + ViewParent parentView4th = parentView3rd.getParent(); + if (!(parentView4th instanceof ViewGroup)) { + return true; + } - // This works without issues for both tablet and phone layouts, - // So no code is needed to check whether the current device is a tablet or phone. + // Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView. + // This only shows in phone layout. + final var touchInsidedView = ((ViewGroup) parentView4th).getChildAt(0); + touchInsidedView.setSoundEffectsEnabled(false); + touchInsidedView.performClick(); - // Close the new Playback speed menu and show the old one. - showOldPlaybackSpeedMenu(); - } catch (Exception ex) { - Logger.printException(() -> "onFlyoutMenuCreate failure", ex); - } - }); + // In tablet layout there is no Dismiss View, instead we just hide all two parent views. + ((ViewGroup) parentView3rd).setVisibility(View.GONE); + ((ViewGroup) parentView4th).setVisibility(View.GONE); + + // Close the litho speed menu and show the old one. + showOldPlaybackSpeedMenu(); + + return true; } public static void showOldPlaybackSpeedMenu() { diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java index 2d6d0f781a..87237ae6ff 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -30,17 +30,31 @@ public static void newVideoStarted(VideoInformation.PlaybackController ignoredPl */ public static void userSelectedPlaybackSpeed(float playbackSpeed) { if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) { - Settings.PLAYBACK_SPEED_DEFAULT.save(playbackSpeed); + // With the 0.05x menu, if the speed is set by integrations 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.MAXIMUM_PLAYBACK_SPEED - 0.05f); // Prevent toast spamming if using the 0.05x adjustments. // Show exactly one toast after the user stops interacting with the speed menu. final long now = System.currentTimeMillis(); lastTimeSpeedChanged = now; + final float finalPlaybackSpeed = playbackSpeed; Utils.runOnMainThreadDelayed(() -> { - if (lastTimeSpeedChanged == now) { - Utils.showToastLong(str("revanced_remember_playback_speed_toast", (playbackSpeed + "x"))); - } // else, the user made additional speed adjustments and this call is outdated. + if (lastTimeSpeedChanged != now) { + // The user made additional speed adjustments and this call is outdated. + return; + } + + if (Settings.PLAYBACK_SPEED_DEFAULT.get() == finalPlaybackSpeed) { + // User changed to a different speed and immediately changed back. + // Or the user is going past 8.0x in the glitched out 0.05x menu. + return; + } + Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed); + + Utils.showToastLong(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x"))); }, TOAST_DELAY_MILLISECONDS); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java index d463cbdd52..89cd7e25c7 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -28,7 +28,9 @@ public class Settings extends BaseSettings { public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", FALSE); public static final IntegerSetting VIDEO_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_video_quality_default_wifi", -2); public static final IntegerSetting VIDEO_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_video_quality_default_mobile", -2); + // Speed public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", FALSE); + 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", 1.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); @@ -378,3 +380,4 @@ public class Settings extends BaseSettings { // endregion } } + 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 83a26c385d..10da13a477 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 @@ -15,6 +15,7 @@ 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 import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter @@ -71,6 +72,7 @@ internal val customPlaybackSpeedPatch = bytecodePatch( addResources("youtube", "video.speed.custom.customPlaybackSpeedPatch") PreferenceScreen.VIDEO.addPreferences( + SwitchPreference("revanced_custom_speed_menu"), TextPreference("revanced_custom_playback_speeds", inputType = InputType.TEXT_MULTI_LINE), ) @@ -108,18 +110,18 @@ internal val customPlaybackSpeedPatch = bytecodePatch( // Override the min/max speeds that can be used. speedLimiterMatch.mutableMethod.apply { - val limiterMinConstIndex = indexOfFirstLiteralInstructionOrThrow(0.25f.toRawBits().toLong()) - var limiterMaxConstIndex = indexOfFirstLiteralInstruction(2.0f.toRawBits().toLong()) + val limitMinIndex = indexOfFirstLiteralInstructionOrThrow(0.25f.toRawBits().toLong()) + var limitMaxIndex = indexOfFirstLiteralInstruction(2.0f.toRawBits().toLong()) // Newer targets have 4x max speed. - if (limiterMaxConstIndex < 0) { - limiterMaxConstIndex = indexOfFirstLiteralInstructionOrThrow(4.0f.toRawBits().toLong()) + if (limitMaxIndex < 0) { + limitMaxIndex = indexOfFirstLiteralInstructionOrThrow(4.0f.toRawBits().toLong()) } - val limiterMinConstDestination = getInstruction(limiterMinConstIndex).registerA - val limiterMaxConstDestination = getInstruction(limiterMaxConstIndex).registerA + val limitMinRegister = getInstruction(limitMinIndex).registerA + val limitMaxRegister = getInstruction(limitMaxIndex).registerA - replaceInstruction(limiterMinConstIndex, "const/high16 v$limiterMinConstDestination, 0.0f") - replaceInstruction(limiterMaxConstIndex, "const/high16 v$limiterMaxConstDestination, 10.0f") + replaceInstruction(limitMinIndex, "const/high16 v$limitMinRegister, 0.0f") + replaceInstruction(limitMaxIndex, "const/high16 v$limitMaxRegister, 8.0f") } // Add a static INSTANCE field to the class. diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index d1fdfd8c81..9dde2e8f35 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1172,8 +1172,11 @@ This is because Crowdin requires temporarily flattening this file and removing t Button is not shown + Custom playback speed menu + Custom speed menu is shown + Custom speed menu is not shown Custom playback speeds - Add or change the available playback speeds + Add or change the custom playback speeds Custom speeds must be less than %s. Using default values. Invalid custom playback speeds. Using default values.