diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index c015c0610..db157c4a1 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -329,7 +329,7 @@ public class Utils { return (R) child; } - throw new IllegalArgumentException("View with resource name '" + str + "' not found"); + throw new IllegalArgumentException("View with resource name not found: " + str); } /** diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt index 72323949c..802c995b3 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt @@ -1,16 +1,20 @@ package app.revanced.extension.youtube +import app.revanced.extension.shared.Logger +import java.util.Collections + /** * generic event provider class */ class Event { - private val eventListeners = mutableSetOf<(T) -> Unit>() + private val eventListeners = Collections.synchronizedSet(mutableSetOf<(T) -> Unit>()) operator fun plusAssign(observer: (T) -> Unit) { addObserver(observer) } fun addObserver(observer: (T) -> Unit) { + Logger.printDebug { "Adding observer: $observer" } eventListeners.add(observer) } @@ -23,7 +27,8 @@ class Event { } operator fun invoke(value: T) { - for (observer in eventListeners) + for (observer in eventListeners) { observer.invoke(value) + } } } 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 99d8a5b6a..96d67a749 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 @@ -1,12 +1,18 @@ package app.revanced.extension.youtube.patches; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.libraries.youtube.innertube.model.media.VideoQuality; import java.lang.ref.WeakReference; +import java.util.Arrays; import java.util.Objects; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.Event; +import app.revanced.extension.youtube.shared.ShortsPlayerState; import app.revanced.extension.youtube.shared.VideoState; /** @@ -16,11 +22,31 @@ import app.revanced.extension.youtube.shared.VideoState; public final class VideoInformation { public interface PlaybackController { - // Methods are added to YT classes during patching. - boolean seekTo(long videoTime); - void seekToRelative(long videoTimeOffset); + // Methods are added during patching. + boolean patch_seekTo(long videoTime); + void patch_seekToRelative(long videoTimeOffset); } + /** + * Interface to use obfuscated methods. + */ + public interface VideoQualityMenuInterface { + // Method is added during patching. + void patch_setQuality(VideoQuality quality); + } + + /** + * Video resolution of the automatic quality option.. + */ + public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2; + + /** + * All quality names are the same for all languages. + * VideoQuality also has a resolution enum that can be used if needed. + */ + public static final String VIDEO_QUALITY_1080P_PREMIUM_NAME = "1080p Premium"; + + private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; /** * Prefix present in all Short player parameters signature. @@ -30,12 +56,10 @@ public final class VideoInformation { private static WeakReference playerControllerRef = new WeakReference<>(null); private static WeakReference mdxPlayerDirectorRef = new WeakReference<>(null); - @NonNull private static String videoId = ""; private static long videoLength = 0; private static long videoTime = -1; - @NonNull private static volatile String playerResponseVideoId = ""; private static volatile boolean playerResponseVideoIdIsShort; private static volatile boolean videoIdIsShort; @@ -45,6 +69,44 @@ public final class VideoInformation { */ private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + private static int desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE; + + private static boolean qualityNeedsUpdating; + + /** + * The available qualities of the current video. + */ + @Nullable + private static VideoQuality[] currentQualities; + + /** + * The current quality of the video playing. + * This is always the actual quality even if Automatic quality is active. + */ + @Nullable + private static VideoQuality currentQuality; + + /** + * The current VideoQualityMenuInterface, set during setVideoQuality. + */ + @Nullable + private static VideoQualityMenuInterface currentMenuInterface; + + /** + * Callback for when the current quality changes. + */ + public static final Event onQualityChange = new Event<>(); + + @Nullable + public static VideoQuality[] getCurrentQualities() { + return currentQualities; + } + + @Nullable + public static VideoQuality getCurrentQuality() { + return currentQuality; + } + /** * Injection point. * @@ -52,12 +114,18 @@ public final class VideoInformation { */ public static void initialize(@NonNull PlaybackController playerController) { try { + Logger.printDebug(() -> "newVideoStarted"); + playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController)); videoTime = -1; videoLength = 0; playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE; + currentQualities = null; + currentMenuInterface = null; + setCurrentQuality(null); } catch (Exception ex) { - Logger.printException(() -> "Failed to initialize", ex); + Logger.printException(() -> "initialize failure", ex); } } @@ -197,14 +265,14 @@ public final class VideoInformation { if (controller == null) { Logger.printDebug(() -> "Cannot seekTo because player controller is null"); } else { - if (controller.seekTo(adjustedSeekTime)) return true; + if (controller.patch_seekTo(adjustedSeekTime)) return true; Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD."); // Else the video is loading or changing videos, or video is casting to a different device. } // Try calling the seekTo method of the MDX player director (called when casting). // The difference has to be a different second mark in order to avoid infinite skip loops - // as the Lounge API only supports seconds. + // as the Lounge API only supports whole seconds. if (adjustedSeekTime / 1000 == videoTime / 1000) { Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small " + "(" + (adjustedSeekTime - videoTime) + "ms)"); @@ -217,9 +285,9 @@ public final class VideoInformation { return false; } - return controller.seekTo(adjustedSeekTime); + return controller.patch_seekTo(adjustedSeekTime); } catch (Exception ex) { - Logger.printException(() -> "Failed to seek", ex); + Logger.printException(() -> "seekTo failure", ex); return false; } } @@ -239,7 +307,7 @@ public final class VideoInformation { if (controller == null) { Logger.printDebug(() -> "Cannot seek relative as player controller is null"); } else { - controller.seekToRelative(seekTime); + controller.patch_seekToRelative(seekTime); } // Adjust the fine adjustment function so it's at least 1 second before/after. @@ -255,10 +323,10 @@ public final class VideoInformation { if (controller == null) { Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null"); } else { - controller.seekToRelative(adjustedSeekTime); + controller.patch_seekToRelative(adjustedSeekTime); } } catch (Exception ex) { - Logger.printException(() -> "Failed to seek relative", ex); + Logger.printException(() -> "seekToRelative failure", ex); } } @@ -373,4 +441,123 @@ public final class VideoInformation { playbackSpeed = newlyLoadedPlaybackSpeed; } } + + /** + * @param resolution The desired video quality resolution to use. + */ + public static void setDesiredVideoResolution(int resolution) { + Utils.verifyOnMainThread(); + Logger.printDebug(() -> "Setting desired video resolution: " + resolution); + desiredVideoResolution = resolution; + qualityNeedsUpdating = true; + } + + private static void setCurrentQuality(@Nullable VideoQuality quality) { + Utils.verifyOnMainThread(); + if (currentQuality != quality) { + Logger.printDebug(() -> "Current quality changed to: " + quality); + currentQuality = quality; + onQualityChange.invoke(quality); + } + } + + /** + * Forcefully changes the video quality of the currently playing video. + */ + public static void changeQuality(VideoQuality quality) { + Utils.verifyOnMainThread(); + + if (currentMenuInterface == null) { + Logger.printException(() -> "Cannot change quality, menu interface is null"); + return; + } + currentMenuInterface.patch_setQuality(quality); + } + + /** + * Injection point. Fixes bad data used by YouTube. + */ + public static int fixVideoQualityResolution(String name, int quality) { + final int correctQuality = 480; + if (name.equals("480p") && quality != correctQuality) { + return correctQuality; + } + + return quality; + } + + /** + * Injection point. + * + * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2 + * @param originalQualityIndex quality index to use, as chosen by YouTube + */ + public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) { + try { + Utils.verifyOnMainThread(); + currentMenuInterface = menu; + + final boolean availableQualitiesChanged = (currentQualities == null) + || !Arrays.equals(currentQualities, qualities); + if (availableQualitiesChanged) { + currentQualities = qualities; + Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities)); + } + + VideoQuality updatedCurrentQuality = qualities[originalQualityIndex]; + if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE + && (currentQuality == null || currentQuality != updatedCurrentQuality)) { + setCurrentQuality(updatedCurrentQuality); + } + + final int preferredQuality = desiredVideoResolution; + if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) { + return originalQualityIndex; // Nothing to do. + } + + // After changing videos the qualities can initially be for the prior video. + // If the qualities have changed and the default is not auto then an update is needed. + if (qualityNeedsUpdating) { + qualityNeedsUpdating = false; + } else if (!availableQualitiesChanged) { + return originalQualityIndex; + } + + // Find the highest quality that is equal to or less than the preferred. + int i = 0; + final int lastQualityIndex = qualities.length - 1; + for (VideoQuality quality : qualities) { + final int qualityResolution = quality.patch_getResolution(); + if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality) + // Use the lowest video quality if the default is lower than all available. + || i == lastQualityIndex) { + final boolean qualityNeedsChange = (i != originalQualityIndex); + Logger.printDebug(() -> qualityNeedsChange + ? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality + : "Video is already the preferred quality: " + quality + ); + + // On first load of a new regular video, if the video is already the + // desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)). + // + // To prevent user confusion, set the video index even if the + // quality is already correct so the UI picker will not display "Auto". + // + // Only change Shorts quality if the quality actually needs to change, + // because the "auto" option is not shown in the flyout + // and setting the same quality again can cause the Short to restart. + if (qualityNeedsChange || !ShortsPlayerState.isOpen()) { + changeQuality(quality); + return i; + } + + return originalQualityIndex; + } + i++; + } + } catch (Exception ex) { + Logger.printException(() -> "setVideoQuality failure", ex); + } + return originalQualityIndex; + } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index e4d1d9817..0ead14314 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -3,12 +3,8 @@ package app.revanced.extension.youtube.patches.playback.quality; import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.Utils.NetworkType; -import androidx.annotation.Nullable; - import com.google.android.libraries.youtube.innertube.model.media.VideoQuality; -import java.util.Arrays; - import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.BooleanSetting; @@ -16,69 +12,15 @@ import app.revanced.extension.shared.settings.IntegerSetting; import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.ShortsPlayerState; -import app.revanced.extension.youtube.videoplayer.VideoQualityDialogButton; @SuppressWarnings("unused") public class RememberVideoQualityPatch { - /** - * Interface to use obfuscated methods. - */ - public interface VideoQualityMenuInterface { - void patch_setQuality(VideoQuality quality); - } - - /** - * Video resolution of the automatic quality option.. - */ - public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2; - - /** - * All quality names are the same for all languages. - * VideoQuality also has a resolution enum that can be used if needed. - */ - public static final String VIDEO_QUALITY_1080P_PREMIUM_NAME = "1080p Premium"; - private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI; private static final IntegerSetting videoQualityMobile = Settings.VIDEO_QUALITY_DEFAULT_MOBILE; private static final IntegerSetting shortsQualityWifi = Settings.SHORTS_QUALITY_DEFAULT_WIFI; private static final IntegerSetting shortsQualityMobile = Settings.SHORTS_QUALITY_DEFAULT_MOBILE; - private static boolean qualityNeedsUpdating; - - /** - * The available qualities of the current video. - */ - @Nullable - private static VideoQuality[] currentQualities; - - /** - * The current quality of the video playing. - * This is always the actual quality even if Automatic quality is active. - */ - @Nullable - private static VideoQuality currentQuality; - - /** - * The current VideoQualityMenuInterface, set during setVideoQuality. - */ - @Nullable - private static VideoQualityMenuInterface currentMenuInterface; - - @Nullable - public static VideoQuality[] getCurrentQualities() { - return currentQualities; - } - - @Nullable - public static VideoQuality getCurrentQuality() { - return currentQuality; - } - - @Nullable - public static VideoQualityMenuInterface getCurrentMenuInterface() { - return currentMenuInterface; - } public static boolean shouldRememberVideoQuality() { BooleanSetting preference = ShortsPlayerState.isOpen() @@ -128,87 +70,12 @@ public class RememberVideoQualityPatch { /** * Injection point. - * - * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2 - * @param originalQualityIndex quality index to use, as chosen by YouTube - */ - public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) { - try { - Utils.verifyOnMainThread(); - currentMenuInterface = menu; - - final boolean availableQualitiesChanged = (currentQualities == null) - || !Arrays.equals(currentQualities, qualities); - if (availableQualitiesChanged) { - currentQualities = qualities; - Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities)); - } - - VideoQuality updatedCurrentQuality = qualities[originalQualityIndex]; - if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE - && (currentQuality == null || currentQuality != updatedCurrentQuality)) { - currentQuality = updatedCurrentQuality; - Logger.printDebug(() -> "Current quality changed to: " + updatedCurrentQuality); - - VideoQualityDialogButton.updateButtonIcon(updatedCurrentQuality); - } - - final int preferredQuality = getDefaultQualityResolution(); - if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) { - return originalQualityIndex; // Nothing to do. - } - - // After changing videos the qualities can initially be for the prior video. - // If the qualities have changed and the default is not auto then an update is needed. - if (!qualityNeedsUpdating && !availableQualitiesChanged) { - return originalQualityIndex; - } - qualityNeedsUpdating = false; - - // Find the highest quality that is equal to or less than the preferred. - int i = 0; - for (VideoQuality quality : qualities) { - final int qualityResolution = quality.patch_getResolution(); - if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality) - // Use the lowest video quality if the default is lower than all available. - || i == qualities.length - 1) { - final boolean qualityNeedsChange = (i != originalQualityIndex); - Logger.printDebug(() -> qualityNeedsChange - ? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality - : "Video is already the preferred quality: " + quality - ); - - // On first load of a new regular video, if the video is already the - // desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)). - // - // To prevent user confusion, set the video index even if the - // quality is already correct so the UI picker will not display "Auto". - // - // Only change Shorts quality if the quality actually needs to change, - // because the "auto" option is not shown in the flyout - // and setting the same quality again can cause the Short to restart. - if (qualityNeedsChange || !ShortsPlayerState.isOpen()) { - menu.patch_setQuality(qualities[i]); - return i; - } - - return originalQualityIndex; - } - i++; - } - } catch (Exception ex) { - Logger.printException(() -> "setVideoQuality failure", ex); - } - return originalQualityIndex; - } - - /** - * Injection point. - * @param userSelectedQualityIndex Element index of {@link #currentQualities}. + * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. */ public static void userChangedShortsQuality(int userSelectedQualityIndex) { try { if (shouldRememberVideoQuality()) { + VideoQuality[] currentQualities = VideoInformation.getCurrentQualities(); if (currentQualities == null) { Logger.printDebug(() -> "Cannot save default quality, qualities is null"); return; @@ -227,6 +94,7 @@ public class RememberVideoQualityPatch { */ public static void userChangedQuality(int videoResolution) { Utils.verifyOnMainThread(); + Logger.printDebug(() -> "User changed quality to: " + videoResolution); if (shouldRememberVideoQuality()) { saveDefaultQuality(videoResolution); @@ -237,27 +105,6 @@ public class RememberVideoQualityPatch { * Injection point. */ public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) { - Utils.verifyOnMainThread(); - - Logger.printDebug(() -> "newVideoStarted"); - currentQualities = null; - currentQuality = null; - currentMenuInterface = null; - qualityNeedsUpdating = true; - - // Hide the quality button until playback starts and the qualities are available. - VideoQualityDialogButton.updateButtonIcon(null); - } - - /** - * Injection point. Fixes bad data used by YouTube. - */ - public static int fixVideoQualityResolution(String name, int quality) { - final int correctQuality = 480; - if (name.equals("480p") && quality != correctQuality) { - return correctQuality; - } - - return quality; + VideoInformation.setDesiredVideoResolution(getDefaultQualityResolution()); } } 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 e738be34d..52e977e34 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 @@ -100,7 +100,8 @@ public class CustomPlaybackSpeedPatch { private static WeakReference currentDialog = new WeakReference<>(null); static { - // Cap at 2 decimals (rounds automatically). + // Use same 2 digit format as built in speed picker, + speedFormatter.setMinimumFractionDigits(2); speedFormatter.setMaximumFractionDigits(2); final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get(); @@ -321,7 +322,7 @@ public class CustomPlaybackSpeedPatch { 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.setText(formatSpeedStringX(currentSpeed)); currentSpeedText.setTextColor(Utils.getAppForegroundColor()); currentSpeedText.setTextSize(16); currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD); @@ -398,10 +399,11 @@ public class CustomPlaybackSpeedPatch { return null; } - VideoInformation.overridePlaybackSpeed(roundedSpeed); - RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed); - currentSpeedText.setText(formatSpeedStringX(roundedSpeed, 2)); // Update display. + currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display. speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider. + + RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed); + VideoInformation.overridePlaybackSpeed(roundedSpeed); return null; }; @@ -437,7 +439,7 @@ public class CustomPlaybackSpeedPatch { 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"). + // For button use 1 digit minimum. speedFormatter.setMinimumFractionDigits(1); // Add buttons for each preset playback speed. @@ -455,7 +457,7 @@ public class CustomPlaybackSpeedPatch { // Create speed button. Button speedButton = new Button(context, null, 0); - speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format. + speedButton.setText(speedFormatter.format(speed)); speedButton.setTextColor(Utils.getAppForegroundColor()); speedButton.setTextSize(12); speedButton.setAllCaps(false); @@ -498,6 +500,9 @@ public class CustomPlaybackSpeedPatch { gridLayout.addView(buttonContainer); } + // Restore 2 digit minimum. + speedFormatter.setMinimumFractionDigits(2); + // Add in-rows speed buttons layout to main layout. mainLayout.addView(gridLayout); @@ -631,8 +636,7 @@ public class CustomPlaybackSpeedPatch { * @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); + private static String formatSpeedStringX(float speed) { return speedFormatter.format(speed) + 'x'; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButton.java index b764ef24a..8e9cc4228 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButton.java @@ -26,6 +26,7 @@ public class CreateSegmentButton { controlsView, "revanced_sb_create_segment_button", null, + null, CreateSegmentButton::shouldBeShown, v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility(), null diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButton.java index 87d46c464..8198a5ba3 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButton.java @@ -28,6 +28,7 @@ public class VotingButton { controlsView, "revanced_sb_voting_button", null, + null, VotingButton::shouldBeShown, v -> SponsorBlockUtils.onVotingClicked(v.getContext()), null diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java index be66cd70a..5568a99b9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java @@ -7,7 +7,6 @@ import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; import app.revanced.extension.youtube.patches.CopyVideoUrlPatch; import app.revanced.extension.youtube.settings.Settings; -import app.revanced.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public class CopyVideoUrlButton { @@ -23,6 +22,7 @@ public class CopyVideoUrlButton { controlsView, "revanced_copy_video_url_button", "revanced_copy_video_url_button_placeholder", + null, Settings.COPY_VIDEO_URL::get, view -> CopyVideoUrlPatch.copyUrl(false), view -> { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java index f7f246e1c..a8f4e7c12 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java @@ -7,7 +7,6 @@ import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; import app.revanced.extension.youtube.patches.CopyVideoUrlPatch; import app.revanced.extension.youtube.settings.Settings; -import app.revanced.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public class CopyVideoUrlTimestampButton { @@ -23,6 +22,7 @@ public class CopyVideoUrlTimestampButton { controlsView, "revanced_copy_video_url_timestamp_button", "revanced_copy_video_url_timestamp_button_placeholder", + null, Settings.COPY_VIDEO_URL_TIMESTAMP::get, view -> CopyVideoUrlPatch.copyUrl(true), view -> { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/ExternalDownloadButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/ExternalDownloadButton.java index 1ba5b2dcc..ae3304ecc 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/ExternalDownloadButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/ExternalDownloadButton.java @@ -23,6 +23,7 @@ public class ExternalDownloadButton { controlsView, "revanced_external_download_button", "revanced_external_download_button_placeholder", + null, Settings.EXTERNAL_DOWNLOADER::get, ExternalDownloadButton::onDownloadClick, null 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 cb4545cfb..60795851c 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 @@ -4,16 +4,26 @@ import android.view.View; import androidx.annotation.Nullable; +import java.text.DecimalFormat; + import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") public class PlaybackSpeedDialogButton { + @Nullable private static PlayerControlButton instance; + private static final DecimalFormat speedDecimalFormatter = new DecimalFormat(); + static { + speedDecimalFormatter.setMinimumFractionDigits(1); + speedDecimalFormatter.setMaximumFractionDigits(2); + } + /** * Injection point. */ @@ -21,8 +31,10 @@ public class PlaybackSpeedDialogButton { try { instance = new PlayerControlButton( controlsView, + "revanced_playback_speed_dialog_button_container", "revanced_playback_speed_dialog_button", "revanced_playback_speed_dialog_button_placeholder", + "revanced_playback_speed_dialog_button_text", Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get, view -> { try { @@ -37,11 +49,11 @@ public class PlaybackSpeedDialogButton { }, view -> { try { + final float defaultSpeed = Settings.PLAYBACK_SPEED_DEFAULT.get(); final float speed = (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() || - VideoInformation.getPlaybackSpeed() == Settings.PLAYBACK_SPEED_DEFAULT.get()) + VideoInformation.getPlaybackSpeed() == defaultSpeed) ? 1.0f - : Settings.PLAYBACK_SPEED_DEFAULT.get(); - + : defaultSpeed; VideoInformation.overridePlaybackSpeed(speed); } catch (Exception ex) { Logger.printException(() -> "speed button reset failure", ex); @@ -49,22 +61,53 @@ public class PlaybackSpeedDialogButton { return true; } ); + + // Set the appropriate icon. + updateButtonAppearance(); } catch (Exception ex) { Logger.printException(() -> "initializeButton failure", ex); } } /** - * injection point + * Injection point. */ public static void setVisibilityImmediate(boolean visible) { - if (instance != null) instance.setVisibilityImmediate(visible); + if (instance != null) { + instance.setVisibilityImmediate(visible); + } } /** - * injection point + * Injection point. */ public static void setVisibility(boolean visible, boolean animated) { - if (instance != null) instance.setVisibility(visible, animated); + if (instance != null) { + instance.setVisibility(visible, animated); + } } -} \ No newline at end of file + + /** + * Injection point. + */ + public static void videoSpeedChanged(float currentVideoSpeed) { + updateButtonAppearance(); + } + + /** + * Updates the button's appearance, including icon and text overlay. + */ + private static void updateButtonAppearance() { + if (instance == null) return; + + try { + Utils.verifyOnMainThread(); + + String speedText = speedDecimalFormatter.format(VideoInformation.getPlaybackSpeed()); + instance.setTextOverlay(speedText); + Logger.printDebug(() -> "Updated playback speed button text to: " + speedText); + } catch (Exception ex) { + Logger.printException(() -> "updateButtonAppearance failure", ex); + } + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java index 94c92b738..548ee86c0 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java @@ -3,6 +3,7 @@ package app.revanced.extension.youtube.videoplayer; import android.view.View; import android.view.animation.Animation; import android.widget.ImageView; +import android.widget.TextView; import androidx.annotation.Nullable; @@ -14,11 +15,12 @@ import app.revanced.extension.youtube.shared.PlayerType; import kotlin.Unit; public class PlayerControlButton { - public interface PlayerControlButtonVisibility { + + public interface PlayerControlButtonStatus { /** * @return If the button should be shown when the player overlay is visible. */ - boolean shouldBeShown(); + boolean buttonEnabled(); } private static final int fadeInDuration; @@ -44,23 +46,46 @@ public class PlayerControlButton { fadeOutImmediate.setDuration(Utils.getResourceInteger("fade_duration_fast")); } + private final WeakReference containerRef; private final WeakReference buttonRef; /** * Empty view with the same layout size as the button. Used to fill empty space while the * fade out animation runs. Without this the chapter titles overlapping the button when fading out. */ private final WeakReference placeHolderRef; - private final PlayerControlButtonVisibility visibilityCheck; + private final WeakReference textOverlayRef; + private final PlayerControlButtonStatus enabledStatus; private boolean isVisible; public PlayerControlButton(View controlsViewGroup, - String imageViewButtonId, + String buttonId, @Nullable String placeholderId, - PlayerControlButtonVisibility buttonVisibility, + @Nullable String textOverlayId, + PlayerControlButtonStatus enabledStatus, View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) { - ImageView imageView = Utils.getChildViewByResourceName(controlsViewGroup, imageViewButtonId); - imageView.setVisibility(View.GONE); + this(controlsViewGroup, buttonId, buttonId, placeholderId, textOverlayId, + enabledStatus, onClickListener, longClickListener); + } + + public PlayerControlButton(View controlsViewGroup, + String viewToHide, + String buttonId, + @Nullable String placeholderId, + @Nullable String textOverlayId, + PlayerControlButtonStatus enabledStatus, + View.OnClickListener onClickListener, + @Nullable View.OnLongClickListener longClickListener) { + View containerView = Utils.getChildViewByResourceName(controlsViewGroup, viewToHide); + containerView.setVisibility(View.GONE); + containerRef = new WeakReference<>(containerView); + + View button = Utils.getChildViewByResourceName(controlsViewGroup, buttonId); + button.setOnClickListener(onClickListener); + if (longClickListener != null) { + button.setOnLongClickListener(longClickListener); + } + buttonRef = new WeakReference<>(button); View tempPlaceholder = null; if (placeholderId != null) { @@ -69,19 +94,19 @@ public class PlayerControlButton { } placeHolderRef = new WeakReference<>(tempPlaceholder); - imageView.setOnClickListener(onClickListener); - if (longClickListener != null) { - imageView.setOnLongClickListener(longClickListener); + TextView tempTextOverlay = null; + if (textOverlayId != null) { + tempTextOverlay = Utils.getChildViewByResourceName(controlsViewGroup, textOverlayId); } + textOverlayRef = new WeakReference<>(tempTextOverlay); - visibilityCheck = buttonVisibility; - buttonRef = new WeakReference<>(imageView); + this.enabledStatus = enabledStatus; isVisible = false; // Update the visibility after the player type changes. // This ensures that button animations are cleared and their states are updated correctly // when switching between states like minimized, maximized, or fullscreen, preventing - // "stuck" animations or incorrect visibility. Without this fix the issue is most noticable + // "stuck" animations or incorrect visibility. Without this fix the issue is most noticeable // when maximizing type 3 miniplayer. PlayerType.getOnChange().addObserver((PlayerType type) -> { playerTypeChanged(type); @@ -111,33 +136,33 @@ public class PlayerControlButton { if (isVisible == visible) return; isVisible = visible; - View button = buttonRef.get(); - if (button == null) return; + View container = containerRef.get(); + if (container == null) return; View placeholder = placeHolderRef.get(); - final boolean shouldBeShown = visibilityCheck.shouldBeShown(); + final boolean buttonEnabled = enabledStatus.buttonEnabled(); - if (visible && shouldBeShown) { - button.clearAnimation(); + if (visible && buttonEnabled) { + container.clearAnimation(); if (animated) { - button.startAnimation(PlayerControlButton.fadeInAnimation); + container.startAnimation(fadeInAnimation); } - button.setVisibility(View.VISIBLE); + container.setVisibility(View.VISIBLE); if (placeholder != null) { placeholder.setVisibility(View.GONE); } } else { - if (button.getVisibility() == View.VISIBLE) { - button.clearAnimation(); + if (container.getVisibility() == View.VISIBLE) { + container.clearAnimation(); if (animated) { - button.startAnimation(PlayerControlButton.fadeOutAnimation); + container.startAnimation(fadeOutAnimation); } - button.setVisibility(View.GONE); + container.setVisibility(View.GONE); } if (placeholder != null) { - placeholder.setVisibility(shouldBeShown + placeholder.setVisibility(buttonEnabled ? View.VISIBLE : View.GONE); } @@ -155,36 +180,37 @@ public class PlayerControlButton { return; } - View button = buttonRef.get(); - if (button == null) return; + View container = containerRef.get(); + if (container == null) return; - button.clearAnimation(); + container.clearAnimation(); View placeholder = placeHolderRef.get(); - if (visibilityCheck.shouldBeShown()) { + if (enabledStatus.buttonEnabled()) { if (isVisible) { - button.setVisibility(View.VISIBLE); + container.setVisibility(View.VISIBLE); if (placeholder != null) placeholder.setVisibility(View.GONE); } else { - button.setVisibility(View.GONE); + container.setVisibility(View.GONE); if (placeholder != null) placeholder.setVisibility(View.VISIBLE); } } else { - button.setVisibility(View.GONE); + container.setVisibility(View.GONE); if (placeholder != null) placeholder.setVisibility(View.GONE); } } public void hide() { + Utils.verifyOnMainThread(); if (!isVisible) return; - Utils.verifyOnMainThread(); - View view = buttonRef.get(); + View view = containerRef.get(); if (view == null) return; view.setVisibility(View.GONE); - view = placeHolderRef.get(); - if (view != null) view.setVisibility(View.GONE); + View placeHolder = placeHolderRef.get(); + if (placeHolder != null) view.setVisibility(View.GONE); + isVisible = false; } @@ -193,50 +219,20 @@ public class PlayerControlButton { * @param resourceId Drawable identifier, or zero to hide the icon. */ public void setIcon(int resourceId) { - try { - View button = buttonRef.get(); - if (button instanceof ImageView imageButton) { - imageButton.setImageResource(resourceId); - } - } catch (Exception ex) { - Logger.printException(() -> "setIcon failure", ex); + View button = buttonRef.get(); + if (button instanceof ImageView imageButton) { + imageButton.setImageResource(resourceId); } } /** - * Starts an animation on the button. - * @param animation The animation to apply. + * Sets the text to be displayed on the text overlay. + * @param text The text to set on the overlay, or null to clear the text. */ - public void startAnimation(Animation animation) { - try { - View button = buttonRef.get(); - if (button != null) { - button.startAnimation(animation); - } - } catch (Exception ex) { - Logger.printException(() -> "startAnimation failure", ex); + public void setTextOverlay(CharSequence text) { + TextView textOverlay = textOverlayRef.get(); + if (textOverlay != null) { + textOverlay.setText(text); } } - - /** - * Clears any animation on the button. - */ - public void clearAnimation() { - try { - View button = buttonRef.get(); - if (button != null) { - button.clearAnimation(); - } - } catch (Exception ex) { - Logger.printException(() -> "clearAnimation failure", ex); - } - } - - /** - * Returns the View associated with this button. - * @return The button View. - */ - public View getView() { - return buttonRef.get(); - } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java index e0a29db5f..a9bd1657b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java @@ -2,9 +2,8 @@ package app.revanced.extension.youtube.videoplayer; import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.Utils.dipToPixels; -import static app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch.AUTOMATIC_VIDEO_QUALITY_VALUE; -import static app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch.VIDEO_QUALITY_1080P_PREMIUM_NAME; -import static app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch.VideoQualityMenuInterface; +import static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE; +import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_1080P_PREMIUM_NAME; import android.app.Dialog; import android.content.Context; @@ -14,6 +13,7 @@ import android.graphics.drawable.shapes.RoundRectShape; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; +import android.text.style.UnderlineSpan; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -39,73 +39,25 @@ import java.util.List; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch; import app.revanced.extension.youtube.settings.Settings; +import kotlin.Unit; @SuppressWarnings("unused") public class VideoQualityDialogButton { - private static final int DRAWABLE_LD = getDrawableIdentifier("revanced_video_quality_dialog_button_ld"); - private static final int DRAWABLE_SD = getDrawableIdentifier("revanced_video_quality_dialog_button_sd"); - private static final int DRAWABLE_HD = getDrawableIdentifier("revanced_video_quality_dialog_button_hd"); - private static final int DRAWABLE_FHD = getDrawableIdentifier("revanced_video_quality_dialog_button_fhd"); - private static final int DRAWABLE_FHD_PLUS = getDrawableIdentifier("revanced_video_quality_dialog_button_fhd_plus"); - private static final int DRAWABLE_QHD = getDrawableIdentifier("revanced_video_quality_dialog_button_qhd"); - private static final int DRAWABLE_4K = getDrawableIdentifier("revanced_video_quality_dialog_button_4k"); - private static final int DRAWABLE_UNKNOWN = getDrawableIdentifier("revanced_video_quality_dialog_button_unknown"); - @Nullable private static PlayerControlButton instance; - /** - * The current resource name of the button icon. - */ - private static int currentIconResource; + @Nullable + private static CharSequence currentOverlayText; - private static int getDrawableIdentifier(String resourceName) { - final int resourceId = Utils.getResourceIdentifier(resourceName, "drawable"); - if (resourceId == 0) Logger.printException(() -> "Could not find resource: " + resourceName); - return resourceId; - } - - /** - * Updates the button icon based on the current video quality. - */ - public static void updateButtonIcon(@Nullable VideoQuality quality) { - try { - Utils.verifyOnMainThread(); - if (instance == null) return; - - final int resolution = quality == null - ? AUTOMATIC_VIDEO_QUALITY_VALUE // Video is still loading. - : quality.patch_getResolution(); - - final int iconResource = switch (resolution) { - case 144, 240, 360 -> DRAWABLE_LD; - case 480 -> DRAWABLE_SD; - case 720 -> DRAWABLE_HD; - case 1080 -> VIDEO_QUALITY_1080P_PREMIUM_NAME.equals(quality.patch_getQualityName()) - ? DRAWABLE_FHD_PLUS - : DRAWABLE_FHD; - case 1440 -> DRAWABLE_QHD; - case 2160 -> DRAWABLE_4K; - default -> DRAWABLE_UNKNOWN; - }; - - if (iconResource != currentIconResource) { - currentIconResource = iconResource; - - Utils.runOnMainThreadDelayed(() -> { - if (iconResource != currentIconResource) { - Logger.printDebug(() -> "Ignoring stale button update to: " + quality); - return; - } - instance.setIcon(iconResource); - }, 100); - } - } catch (Exception ex) { - Logger.printException(() -> "updateButtonIcon failure", ex); - } + static { + VideoInformation.onQualityChange.addObserver((@Nullable VideoQuality quality) -> { + updateButtonText(quality); + return Unit.INSTANCE; + }); } /** @@ -115,8 +67,10 @@ public class VideoQualityDialogButton { try { instance = new PlayerControlButton( controlsView, + "revanced_video_quality_dialog_button_container", "revanced_video_quality_dialog_button", "revanced_video_quality_dialog_button_placeholder", + "revanced_video_quality_dialog_button_text", Settings.VIDEO_QUALITY_DIALOG_BUTTON::get, view -> { try { @@ -127,9 +81,8 @@ public class VideoQualityDialogButton { }, view -> { try { - VideoQuality[] qualities = RememberVideoQualityPatch.getCurrentQualities(); - VideoQualityMenuInterface menu = RememberVideoQualityPatch.getCurrentMenuInterface(); - if (qualities == null || menu == null) { + VideoQuality[] qualities = VideoInformation.getCurrentQualities(); + if (qualities == null) { Logger.printDebug(() -> "Cannot reset quality, videoQualities is null"); return true; } @@ -140,7 +93,7 @@ public class VideoQualityDialogButton { final int resolution = quality.patch_getResolution(); if (resolution != AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= defaultResolution) { Logger.printDebug(() -> "Resetting quality to: " + quality); - menu.patch_setQuality(quality); + VideoInformation.changeQuality(quality); return true; } } @@ -156,8 +109,8 @@ public class VideoQualityDialogButton { } ); - // Set initial icon. - updateButtonIcon(RememberVideoQualityPatch.getCurrentQuality()); + // Set initial text. + updateButtonText(VideoInformation.getCurrentQuality()); } catch (Exception ex) { Logger.printException(() -> "initializeButton failure", ex); } @@ -181,13 +134,56 @@ public class VideoQualityDialogButton { } } + /** + * Updates the button text based on the current video quality. + */ + public static void updateButtonText(@Nullable VideoQuality quality) { + try { + Utils.verifyOnMainThread(); + if (instance == null) return; + + final int resolution = quality == null + ? AUTOMATIC_VIDEO_QUALITY_VALUE // Video is still loading. + : quality.patch_getResolution(); + + SpannableStringBuilder text = new SpannableStringBuilder(); + String qualityText = switch (resolution) { + case AUTOMATIC_VIDEO_QUALITY_VALUE -> ""; + case 144, 240, 360 -> "LD"; + case 480 -> "SD"; + case 720 -> "HD"; + case 1080 -> "FHD"; + case 1440 -> "QHD"; + case 2160 -> "4K"; + default -> "?"; // Should never happen. + }; + + text.append(qualityText); + if (resolution == 1080 && VIDEO_QUALITY_1080P_PREMIUM_NAME.equals(quality.patch_getQualityName())) { + // Underline the entire "FHD" text for 1080p Premium. + text.setSpan(new UnderlineSpan(), 0, qualityText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + currentOverlayText = text; + Utils.runOnMainThreadDelayed(() -> { + if (currentOverlayText != text) { + Logger.printDebug(() -> "Ignoring stale button text update of: " + text); + return; + } + instance.setTextOverlay(text); + }, 100); + } catch (Exception ex) { + Logger.printException(() -> "updateButtonText failure", ex); + } + } + /** * Shows a dialog with available video qualities, excluding Auto, with a title showing the current quality. */ private static void showVideoQualityDialog(Context context) { try { - VideoQuality[] currentQualities = RememberVideoQualityPatch.getCurrentQualities(); - VideoQuality currentQuality = RememberVideoQualityPatch.getCurrentQuality(); + VideoQuality[] currentQualities = VideoInformation.getCurrentQualities(); + VideoQuality currentQuality = VideoInformation.getCurrentQuality(); if (currentQualities == null || currentQuality == null) { Logger.printDebug(() -> "Cannot show qualities dialog, videoQualities is null"); return; @@ -198,12 +194,6 @@ public class VideoQualityDialogButton { return; } - VideoQualityMenuInterface menu = RememberVideoQualityPatch.getCurrentMenuInterface(); - if (menu == null) { - Logger.printDebug(() -> "Cannot show qualities dialog, menu is null"); - return; - } - // -1 adjustment for automatic quality at first index. int listViewSelectedIndex = -1; for (VideoQuality quality : currentQualities) { @@ -317,15 +307,8 @@ public class VideoQualityDialogButton { try { final int originalIndex = which + 1; // Adjust for automatic. VideoQuality selectedQuality = currentQualities[originalIndex]; - Logger.printDebug(() -> "User clicked on quality: " + selectedQuality); - - if (RememberVideoQualityPatch.shouldRememberVideoQuality()) { - RememberVideoQualityPatch.saveDefaultQuality(selectedQuality.patch_getResolution()); - } - // Don't update button icon now. Icon will update when the actual - // quality is changed by YT. This is needed to ensure the icon is correct - // if YT ignores changing from 1080p Premium to regular 1080p. - menu.patch_setQuality(selectedQuality); + RememberVideoQualityPatch.userChangedQuality(selectedQuality.patch_getResolution()); + VideoInformation.changeQuality(selectedQuality); dialog.dismiss(); } catch (Exception ex) { @@ -356,9 +339,12 @@ public class VideoQualityDialogButton { portraitWidth = Math.min( portraitWidth, context.getResources().getDisplayMetrics().heightPixels); + // Limit height in landscape mode. + params.height = Utils.percentageHeightToPixels(80); + } else { + params.height = WindowManager.LayoutParams.WRAP_CONTENT; } params.width = portraitWidth; - params.height = WindowManager.LayoutParams.WRAP_CONTENT; window.setAttributes(params); window.setBackgroundDrawable(null); } @@ -428,6 +414,15 @@ public class VideoQualityDialogButton { } private static class CustomQualityAdapter extends ArrayAdapter { + private static final int CUSTOM_LIST_ITEM_CHECKED_ID = Utils.getResourceIdentifier( + "revanced_custom_list_item_checked", "layout"); + private static final int CHECK_ICON_ID = Utils.getResourceIdentifier( + "revanced_check_icon", "id"); + private static final int CHECK_ICON_PLACEHOLDER_ID = Utils.getResourceIdentifier( + "revanced_check_icon_placeholder", "id"); + private static final int ITEM_TEXT_ID = Utils.getResourceIdentifier( + "revanced_item_text", "id"); + private int selectedPosition = -1; public CustomQualityAdapter(@NonNull Context context, @NonNull List objects) { @@ -446,20 +441,14 @@ public class VideoQualityDialogButton { if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate( - Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"), + CUSTOM_LIST_ITEM_CHECKED_ID, parent, false ); viewHolder = new ViewHolder(); - viewHolder.checkIcon = convertView.findViewById( - Utils.getResourceIdentifier("revanced_check_icon", "id") - ); - viewHolder.placeholder = convertView.findViewById( - Utils.getResourceIdentifier("revanced_check_icon_placeholder", "id") - ); - viewHolder.textView = convertView.findViewById( - Utils.getResourceIdentifier("revanced_item_text", "id") - ); + viewHolder.checkIcon = convertView.findViewById(CHECK_ICON_ID); + viewHolder.placeholder = convertView.findViewById(CHECK_ICON_PLACEHOLDER_ID); + viewHolder.textView = convertView.findViewById(ITEM_TEXT_ID); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); diff --git a/patches/api/patches.api b/patches/api/patches.api index 91f78e8db..db1bb7e8e 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -1660,12 +1660,12 @@ public final class app/revanced/patches/youtube/video/quality/RememberVideoQuali public static final fun getRememberVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } -public final class app/revanced/patches/youtube/video/quality/VideoQualityPatchKt { - public static final fun getVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +public final class app/revanced/patches/youtube/video/quality/VideoQualityDialogButtonPatchKt { + public static final fun getVideoQualityDialogButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } -public final class app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatchKt { - public static final fun getVideoQualityButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +public final class app/revanced/patches/youtube/video/quality/VideoQualityPatchKt { + public static final fun getVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } public final class app/revanced/patches/youtube/video/speed/PlaybackSpeedPatchKt { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt index 7db8de5b2..7eb292bfd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt @@ -282,7 +282,7 @@ val playerControlsPatch = bytecodePatch( // The change to support this is simple and only requires adding buttons to both layout files, // but for now force this different layout off since it's still an experimental test. if (is_19_35_or_greater) { - playerBottomControlsExploderFeatureFlagFingerprint.method.returnEarly() + playerBottomControlsExploderFeatureFlagFingerprint.method.returnLate(false) } // A/B test of new top overlay controls. Two different layouts can be used: diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt index 40110878c..8a3ee4e92 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt @@ -70,87 +70,84 @@ val forceOriginalAudioPatch = bytecodePatch( ) } - val isDefaultAudioTrackMethod = formatStreamModelToStringFingerprint.originalMethod - .findMethodFromToString("isDefaultAudioTrack=") - val audioTrackDisplayNameMethod = formatStreamModelToStringFingerprint.originalMethod - .findMethodFromToString("audioTrackDisplayName=") - val audioTrackIdMethod = formatStreamModelToStringFingerprint.originalMethod - .findMethodFromToString("audioTrackId=") + formatStreamModelToStringFingerprint.let { + val isDefaultAudioTrackMethod = it.originalMethod.findMethodFromToString("isDefaultAudioTrack=") + val audioTrackDisplayNameMethod = it.originalMethod.findMethodFromToString("audioTrackDisplayName=") + val audioTrackIdMethod = it.originalMethod.findMethodFromToString("audioTrackId=") - proxy(classes.first { - it.type == audioTrackIdMethod.definingClass - }).mutableClass.apply { - // Add a new field to store the override. - val helperFieldName = "isDefaultAudioTrackOverride" - fields.add( - ImmutableField( - type, - helperFieldName, - "Ljava/lang/Boolean;", - // Boolean is a 100% immutable class (all fields are final) - // and safe to write to a shared field without volatile/synchronization, - // but without volatile the field can show stale data - // and the same field is calculated more than once by different threads. - AccessFlags.PRIVATE.value or AccessFlags.VOLATILE.value, + it.classDef.apply { + // Add a new field to store the override. + val helperFieldName = "patch_isDefaultAudioTrackOverride" + fields.add( + ImmutableField( + type, + helperFieldName, + "Ljava/lang/Boolean;", + // Boolean is a 100% immutable class (all fields are final) + // and safe to write to a shared field without volatile/synchronization, + // but without volatile the field can show stale data + // and the same field is calculated more than once by different threads. + AccessFlags.PRIVATE.value or AccessFlags.VOLATILE.value, + null, + null, + null + ).toMutable() + ) + + // Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed. + val helperMethodClass = type + val helperMethodName = "patch_isDefaultAudioTrack" + val helperMethod = ImmutableMethod( + helperMethodClass, + helperMethodName, + listOf(ImmutableMethodParameter("Z", null, null)), + "Z", + AccessFlags.PRIVATE.value, null, null, - null - ).toMutable() - ) + MutableMethodImplementation(6), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean; + if-eqz v0, :call_extension + invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z + move-result v3 + return v3 + + :call_extension + invoke-virtual { p0 }, $audioTrackIdMethod + move-result-object v1 + + invoke-virtual { p0 }, $audioTrackDisplayNameMethod + move-result-object v2 + + invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z + move-result v3 + + invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; + move-result-object v0 + iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean; + return v3 + """ + ) + } + methods.add(helperMethod) - // Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed. - val helperMethodClass = type - val helperMethodName = "patch_isDefaultAudioTrack" - val helperMethod = ImmutableMethod( - helperMethodClass, - helperMethodName, - listOf(ImmutableMethodParameter("Z", null, null)), - "Z", - AccessFlags.PRIVATE.value, - null, - null, - MutableMethodImplementation(6), - ).toMutable().apply { - addInstructionsWithLabels( - 0, - """ - iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean; - if-eqz v0, :call_extension - invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z - move-result v3 - return v3 - - :call_extension - invoke-virtual { p0 }, $audioTrackIdMethod - move-result-object v1 - - invoke-virtual { p0 }, $audioTrackDisplayNameMethod - move-result-object v2 - - invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z - move-result v3 - - invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; - move-result-object v0 - iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean; - return v3 - """ - ) - } - methods.add(helperMethod) + // Modify isDefaultAudioTrack() to call extension helper method. + isDefaultAudioTrackMethod.apply { + val index = indexOfFirstInstructionOrThrow(Opcode.RETURN) + val register = getInstruction(index).registerA - // Modify isDefaultAudioTrack() to call extension helper method. - isDefaultAudioTrackMethod.apply { - val index = indexOfFirstInstructionOrThrow(Opcode.RETURN) - val register = getInstruction(index).registerA - - addInstructions( - index, - """ - invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z - move-result v$register - """ - ) + addInstructions( + index, + """ + invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z + move-result v$register + """ + ) + } } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/hdr/DisableHdrPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/hdr/DisableHdrPatch.kt index bfa9cbd2e..5d8e6d1c0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/hdr/DisableHdrPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/hdr/DisableHdrPatch.kt @@ -63,7 +63,7 @@ val disableHdrPatch = bytecodePatch( return v0 :useHdr nop - """ + """ ) } } 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 7d711aec8..74b0e0864 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 @@ -133,3 +133,46 @@ internal val playbackSpeedClassFingerprint = fingerprint { ) strings("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT") } + + +internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;" + +internal val videoQualityFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters( + "I", // Resolution. + "Ljava/lang/String;", // Human readable resolution: "480p", "1080p Premium", etc + "Z", + "L" + ) + custom { _, classDef -> + classDef.type == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE + } +} + +internal val videoQualitySetterFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("[L", "I", "Z") + opcodes( + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IPUT_BOOLEAN, + ) + strings("menu_item_video_quality") +} + +/** + * Matches with the class found in [videoQualitySetterFingerprint]. + */ +internal val setVideoQualityFingerprint = fingerprint { + returns("V") + parameters("L") + opcodes( + Opcode.IGET_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + ) +} 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 99f0c75de..368aff634 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 @@ -38,7 +38,9 @@ import com.android.tools.smali.dexlib2.util.MethodUtil private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/VideoInformation;" private const val EXTENSION_PLAYER_INTERFACE = - "Lapp/revanced/extension/youtube/patches/VideoInformation${'$'}PlaybackController;" + "Lapp/revanced/extension/youtube/patches/VideoInformation\$PlaybackController;" +private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE = + "Lapp/revanced/extension/youtube/patches/VideoInformation\$VideoQualityMenuInterface;" private lateinit var playerInitMethod: MutableMethod private var playerInitInsertIndex = -1 @@ -83,7 +85,6 @@ val videoInformationPatch = bytecodePatch( ) execute { - playerInitMethod = playerInitFingerprint.classDef.methods.first { MethodUtil.isConstructor(it) } // Find the location of the first invoke-direct call and extract the register storing the 'this' object reference. @@ -93,9 +94,6 @@ val videoInformationPatch = bytecodePatch( playerInitInsertRegister = playerInitMethod.getInstruction(initThisIndex).registerC playerInitInsertIndex = initThisIndex + 1 - // Hook the player controller for use through the extension. - onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize") - val seekFingerprintResultMethod = seekFingerprint.match(playerInitFingerprint.originalClassDef).method val seekRelativeFingerprintResultMethod = seekRelativeFingerprint.match(playerInitFingerprint.originalClassDef).method @@ -272,6 +270,131 @@ val videoInformationPatch = bytecodePatch( speedSelectionValueRegister = getInstruction(index).registerA } + videoQualityFingerprint.let { + // Fix bad data used by YouTube. + it.method.addInstructions( + 0, + """ + invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I + move-result p1 + """ + ) + + // Add methods to access obfuscated quality fields. + it.classDef.apply { + methods.add( + ImmutableMethod( + type, + "patch_getQualityName", + listOf(), + "Ljava/lang/String;", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + // Only one string field. + val qualityNameField = fields.single { field -> + field.type == "Ljava/lang/String;" + } + + addInstructions( + 0, + """ + iget-object v0, p0, $qualityNameField + return-object v0 + """ + ) + } + ) + + methods.add( + ImmutableMethod( + type, + "patch_getResolution", + listOf(), + "I", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + val resolutionField = fields.single { field -> + field.type == "I" + } + + addInstructions( + 0, + """ + iget v0, p0, $resolutionField + return v0 + """ + ) + } + ) + } + } + + // Detect video quality changes and override the current quality. + setVideoQualityFingerprint.match( + videoQualitySetterFingerprint.originalClassDef + ).let { match -> + // This instruction refers to the field with the type that contains the setQuality method. + val onItemClickListenerClassReference = match.method + .getInstruction(0).reference + val setQualityFieldReference = match.method + .getInstruction(1).reference as FieldReference + + proxy( + classes.find { classDef -> + classDef.type == setQualityFieldReference.type + }!! + ).mutableClass.apply { + // Add interface and helper methods to allow extension code to call obfuscated methods. + interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE) + + methods.add( + ImmutableMethod( + type, + "patch_setQuality", + listOf( + ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null) + ), + "V", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + val setQualityMenuIndexMethod = methods.single { method -> + method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE + } + + addInstructions( + 0, + """ + invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod + return-void + """ + ) + } + ) + } + + videoQualitySetterFingerprint.method.addInstructions( + 0, + """ + # Get object instance to invoke setQuality method. + iget-object v0, p0, $onItemClickListenerClassReference + iget-object v0, v0, $setQualityFieldReference + + invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I + move-result p2 + """ + ) + } + + onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize") videoSpeedChangedHook(EXTENSION_CLASS_DESCRIPTOR, "videoSpeedChanged") userSelectedPlaybackSpeedHook(EXTENSION_CLASS_DESCRIPTOR, "userSelectedPlaybackSpeed") } @@ -282,8 +405,8 @@ private fun addSeekInterfaceMethods(targetClass: MutableClass, seekToMethod: Met targetClass.interfaces.add(EXTENSION_PLAYER_INTERFACE) arrayOf( - Triple(seekToMethod, "seekTo", true), - Triple(seekToRelativeMethod, "seekToRelative", false), + Triple(seekToMethod, "patch_seekTo", true), + Triple(seekToRelativeMethod, "patch_seekToRelative", false), ).forEach { (method, name, returnsBoolean) -> // Add interface method. // Get enum type for the seek helper method. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt index f04079632..09ec88f9c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt @@ -5,34 +5,6 @@ import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;" - -internal val videoQualityFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) - parameters( - "I", // Resolution. - "Ljava/lang/String;", // Human readable resolution: "480p", "1080p Premium", etc - "Z", - "L" - ) - custom { _, classDef -> - classDef.type == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE - } -} - -/** - * Matches with the class found in [videoQualitySetterFingerprint]. - */ -internal val setVideoQualityFingerprint = fingerprint { - returns("V") - parameters("L") - opcodes( - Opcode.IGET_OBJECT, - Opcode.IPUT_OBJECT, - Opcode.IGET_OBJECT, - ) -} - internal val videoQualityItemOnClickParentFingerprint = fingerprint { returns("V") strings("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT") @@ -54,19 +26,6 @@ internal val videoQualityItemOnClickFingerprint = fingerprint { } } -internal val videoQualitySetterFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returns("V") - parameters("[L", "I", "Z") - opcodes( - Opcode.IF_EQZ, - Opcode.INVOKE_VIRTUAL, - Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_VIRTUAL, - Opcode.IPUT_BOOLEAN, - ) - strings("menu_item_video_quality") -} internal val videoQualityMenuOptionsFingerprint = fingerprint { accessFlags(AccessFlags.STATIC) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 7a31a6b74..2f67487c8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -1,10 +1,8 @@ package app.revanced.patches.youtube.video.quality import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.ListPreference @@ -15,18 +13,10 @@ import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint import app.revanced.patches.youtube.video.information.onCreateHook import app.revanced.patches.youtube.video.information.videoInformationPatch -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch;" -private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE = - "Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch\$VideoQualityMenuInterface;" val rememberVideoQualityPatch = bytecodePatch { dependsOn( @@ -69,131 +59,6 @@ val rememberVideoQualityPatch = bytecodePatch { onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted") - videoQualityFingerprint.let { - // Fix bad data used by YouTube. - it.method.addInstructions( - 0, - """ - invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I - move-result p1 - """ - ) - - // Add methods to access obfuscated quality fields. - it.classDef.apply { - methods.add( - ImmutableMethod( - type, - "patch_getQualityName", - listOf(), - "Ljava/lang/String;", - AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, - null, - null, - MutableMethodImplementation(2), - ).toMutable().apply { - // Only one string field. - val qualityNameField = fields.single { field -> - field.type == "Ljava/lang/String;" - } - - addInstructions( - 0, - """ - iget-object v0, p0, $qualityNameField - return-object v0 - """ - ) - } - ) - - methods.add( - ImmutableMethod( - type, - "patch_getResolution", - listOf(), - "I", - AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, - null, - null, - MutableMethodImplementation(2), - ).toMutable().apply { - val resolutionField = fields.single { field -> - field.type == "I" - } - - addInstructions( - 0, - """ - iget v0, p0, $resolutionField - return v0 - """ - ) - } - ) - } - } - - // Inject a call to set the remembered quality once a video loads. - setVideoQualityFingerprint.match( - videoQualitySetterFingerprint.originalClassDef - ).let { match -> - // This instruction refers to the field with the type that contains the setQuality method. - val instructions = match.method.implementation!!.instructions - val onItemClickListenerClassReference = - (instructions.elementAt(0) as ReferenceInstruction).reference - val setQualityFieldReference = - ((instructions.elementAt(1) as ReferenceInstruction).reference) as FieldReference - - proxy( - classes.find { classDef -> - classDef.type == setQualityFieldReference.type - }!! - ).mutableClass.apply { - // Add interface and helper methods to allow extension code to call obfuscated methods. - interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE) - - methods.add( - ImmutableMethod( - type, - "patch_setQuality", - listOf( - ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null) - ), - "V", - AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, - null, - null, - MutableMethodImplementation(2), - ).toMutable().apply { - val setQualityMenuIndexMethod = methods.single { method -> - method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE - } - - addInstructions( - 0, - """ - invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod - return-void - """ - ) - } - ) - } - - videoQualitySetterFingerprint.method.addInstructions( - 0, - """ - # Get object instance to invoke setQuality method. - iget-object v0, p0, $onItemClickListenerClassReference - iget-object v0, v0, $setQualityFieldReference - - invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I - move-result p2 - """ - ) - } - // Inject a call to remember the selected quality for Shorts. videoQualityItemOnClickFingerprint.match( videoQualityItemOnClickParentFingerprint.classDef diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityDialogButtonPatch.kt similarity index 70% rename from patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityDialogButtonPatch.kt index aaa42c827..d2618bb63 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityDialogButtonPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.video.quality.button +package app.revanced.patches.youtube.video.quality import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch @@ -9,7 +9,6 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playercontrols.* import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch -import app.revanced.patches.youtube.video.quality.rememberVideoQualityPatch import app.revanced.util.ResourceGroup import app.revanced.util.copyResources @@ -21,14 +20,7 @@ private val videoQualityButtonResourcePatch = resourcePatch { "qualitybutton", ResourceGroup( "drawable", - "revanced_video_quality_dialog_button_ld.xml", - "revanced_video_quality_dialog_button_sd.xml", - "revanced_video_quality_dialog_button_hd.xml", - "revanced_video_quality_dialog_button_fhd.xml", - "revanced_video_quality_dialog_button_fhd_plus.xml", - "revanced_video_quality_dialog_button_qhd.xml", - "revanced_video_quality_dialog_button_4k.xml", - "revanced_video_quality_dialog_button_unknown.xml", + "revanced_video_quality_dialog_button_rectangle.xml", ), ) @@ -39,7 +31,7 @@ private val videoQualityButtonResourcePatch = resourcePatch { private const val QUALITY_BUTTON_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/videoplayer/VideoQualityDialogButton;" -val videoQualityButtonPatch = bytecodePatch( +val videoQualityDialogButtonPatch = bytecodePatch( description = "Adds the option to display video quality dialog button in the video player.", ) { dependsOn( @@ -52,7 +44,7 @@ val videoQualityButtonPatch = bytecodePatch( ) execute { - addResources("youtube", "video.quality.button.videoQualityButtonPatch") + addResources("youtube", "video.quality.button.videoQualityDialogButtonPatch") PreferenceScreen.PLAYER.addPreferences( SwitchPreference("revanced_video_quality_dialog_button"), diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt index 432b4f8f3..55a449647 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt @@ -5,7 +5,6 @@ import app.revanced.patches.shared.misc.settings.preference.BasePreference import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.patches.youtube.video.quality.button.videoQualityButtonPatch /** * Video quality settings. Used to organize all speed related settings together. @@ -20,7 +19,7 @@ val videoQualityPatch = bytecodePatch( dependsOn( rememberVideoQualityPatch, advancedVideoQualityMenuPatch, - videoQualityButtonPatch, + videoQualityDialogButtonPatch, ) compatibleWith( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch.kt index 0ac9b0769..4885aa0fe 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch.kt @@ -9,6 +9,9 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playercontrols.* import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.video.information.userSelectedPlaybackSpeedHook +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.patches.youtube.video.information.videoSpeedChangedHook import app.revanced.patches.youtube.video.speed.custom.customPlaybackSpeedPatch import app.revanced.util.ResourceGroup import app.revanced.util.copyResources @@ -21,8 +24,8 @@ private val playbackSpeedButtonResourcePatch = resourcePatch { "speedbutton", ResourceGroup( "drawable", - "revanced_playback_speed_dialog_button.xml", - ), + "revanced_playback_speed_dialog_button_rectangle.xml" + ) ) addBottomControl("speedbutton") @@ -42,6 +45,7 @@ val playbackSpeedButtonPatch = bytecodePatch( customPlaybackSpeedPatch, playbackSpeedButtonResourcePatch, playerControlsPatch, + videoInformationPatch, ) execute { @@ -53,5 +57,8 @@ val playbackSpeedButtonPatch = bytecodePatch( initializeBottomControl(SPEED_BUTTON_CLASS_DESCRIPTOR) injectVisibilityCheckCall(SPEED_BUTTON_CLASS_DESCRIPTOR) + + videoSpeedChangedHook(SPEED_BUTTON_CLASS_DESCRIPTOR, "videoSpeedChanged") + userSelectedPlaybackSpeedHook(SPEED_BUTTON_CLASS_DESCRIPTOR, "videoSpeedChanged") } } diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 3624d7f22..351d184ec 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1543,7 +1543,7 @@ Enabling this can unlock higher video qualities" Button is shown. Tap and hold to reset playback speed to default Button is not shown - + Show video quality button Button is shown. Tap and hold to reset quality to default Button is not shown diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml deleted file mode 100644 index 22051e3fb..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml deleted file mode 100644 index b7a8f5c73..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd_plus.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd_plus.xml deleted file mode 100644 index 18e163ca2..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd_plus.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml deleted file mode 100644 index db2bad1b1..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_ld.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_ld.xml deleted file mode 100644 index a49f5a200..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_ld.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml deleted file mode 100644 index 7609d368e..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_rectangle.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_rectangle.xml new file mode 100644 index 000000000..2791f1990 --- /dev/null +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_rectangle.xml @@ -0,0 +1,9 @@ + + + diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_sd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_sd.xml deleted file mode 100644 index 150ede850..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_sd.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_unknown.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_unknown.xml deleted file mode 100644 index 4e5750cfe..000000000 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_unknown.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml index deb534fba..6a8772d8a 100644 --- a/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml +++ b/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml @@ -6,22 +6,38 @@ android:layout_height="wrap_content" android:layoutDirection="ltr"> - + yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button"> - + + + + + + diff --git a/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button.xml b/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button.xml deleted file mode 100644 index 114da2a5d..000000000 --- a/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button_rectangle.xml b/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button_rectangle.xml new file mode 100644 index 000000000..2791f1990 --- /dev/null +++ b/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button_rectangle.xml @@ -0,0 +1,9 @@ + + + diff --git a/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml index c0109ebc5..c7532ecd6 100644 --- a/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml +++ b/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml @@ -6,23 +6,38 @@ android:layout_height="wrap_content" android:layoutDirection="ltr"> - + yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button"> - + + + + + +