From 0bb86694e24a6a41edee62f5ef1bb80fe7bc3f19 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:47:29 +0200 Subject: [PATCH] fix(YouTube - Return YouTube Dislike): Prevent the first Short opened from freezing the UI (#532) --- .../patches/ReturnYouTubeDislikePatch.java | 59 ++++++++++++++----- .../patches/VideoInformation.java | 46 +++++++++++++-- .../ReturnYouTubeDislikeFilterPatch.java | 6 +- .../patches/spoof/SpoofSignaturePatch.java | 9 +-- .../requests/ReturnYouTubeDislikeApi.java | 4 +- .../ReturnYouTubeDislikeSettingsFragment.java | 6 +- .../integrations/shared/PlayerType.kt | 7 ++- 7 files changed, 98 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java index 154088daee..38e4e54dc8 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch; +import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.shared.PlayerType; @@ -27,19 +28,25 @@ * Handles all interaction of UI patch components. * * Known limitation: - * Litho based Shorts player can experience temporarily frozen video playback if the RYD fetch takes too long. + * The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed. + * This is because it modifies the dislikes text synchronously, and if the RYD fetch has + * not completed yet then the UI will be temporarily frozen. * - * Temporary work around: - * Enable app spoofing to version 18.33.40 or older, as that uses a non litho Shorts player. - * - * Permanent fix (yet to be implemented), either of: - * - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously. - * - Find a way to force Litho to rebuild it's component tree - * (and use that hook to force the shorts dislikes to update after the fetch is completed). + * A (yet to be implemented) solution that fixes this problem. Any one of: + * - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously. + * - Find a way to force Litho to rebuild it's component tree, + * and use that hook to force the shorts dislikes to update after the fetch is completed. + * - Hook into the dislikes button image view, and replace the dislikes thumb down image with a + * generated image of the number of dislikes, then update the image asynchronously. This Could + * also be used for the regular video player to give a better UI layout and completely remove + * the need for the Rolling Number patches. */ @SuppressWarnings("unused") public class ReturnYouTubeDislikePatch { + public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER = + SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40"); + /** * RYD data for the current video on screen. */ @@ -549,26 +556,46 @@ private static boolean isShortTextViewOnScreen(@NonNull View view) { // Video Id and voting hooks (all players). // + private static volatile boolean lastPlayerResponseWasShort; + /** * Injection point. Uses 'playback response' video id hook to preload RYD. */ - public static void preloadVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) { + public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { try { - // Shorts shelf in home and subscription feed causes player response hook to be called, - // and the 'is opening/playing' parameter will be false. - // This hook will be called again when the Short is actually opened. - if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_ENABLED.getBoolean()) { + if (!SettingsEnum.RYD_ENABLED.getBoolean()) { return; } - if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) { + if (videoId.equals(lastPrefetchedVideoId)) { return; } - if (videoId.equals(lastPrefetchedVideoId)) { + + final boolean videoIdIsShort = VideoInformation.lastVideoIdIsShort(); + // Shorts shelf in home and subscription feed causes player response hook to be called, + // and the 'is opening/playing' parameter will be false. + // This hook will be called again when the Short is actually opened. + if (videoIdIsShort && (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean())) { return; } + final boolean waitForFetchToComplete = !IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER + && videoIdIsShort && !lastPlayerResponseWasShort; + lastPlayerResponseWasShort = videoIdIsShort; lastPrefetchedVideoId = videoId; + LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId); - ReturnYouTubeDislike.getFetchForVideoId(videoId); + ReturnYouTubeDislike fetch = ReturnYouTubeDislike.getFetchForVideoId(videoId); + if (waitForFetchToComplete && !fetch.fetchCompleted()) { + // This call is off the main thread, so wait until the RYD fetch completely finishes, + // otherwise if this returns before the fetch completes then the UI can + // become frozen when the main thread tries to modify the litho Shorts dislikes and + // it must wait for the fetch. + // Only need to do this for the first Short opened, as the next Short to swipe to + // are preloaded in the background. + // + // If an asynchronous litho Shorts solution is found, then this blocking call should be removed. + LogHelper.printDebug(() -> "Waiting for prefetch to complete: " + videoId); + fetch.getFetchData(10000); // Use any arbitrarily large max wait time. + } } catch (Exception ex) { LogHelper.printException(() -> "preloadVideoId failure", ex); } diff --git a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java index 81ae8ce291..a6ba5649cd 100644 --- a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java @@ -17,6 +17,10 @@ public final class VideoInformation { private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; private static final String SEEK_METHOD_NAME = "seekTo"; + /** + * Prefix present in all Short player parameters signature. + */ + private static final String SHORTS_PLAYER_PARAMETERS = "8AEB"; private static WeakReference playerControllerRef; private static Method seekMethod; @@ -28,6 +32,7 @@ public final class VideoInformation { @NonNull private static volatile String playerResponseVideoId = ""; + private static volatile boolean videoIdIsShort; /** * The current playback speed @@ -65,12 +70,33 @@ public static void setVideoId(@NonNull String newlyLoadedVideoId) { } } + /** + * @return If the player parameters are for a Short. + */ + public static boolean playerParametersAreShort(@NonNull String parameters) { + return parameters.startsWith(SHORTS_PLAYER_PARAMETERS); + } + + /** + * Injection point. + */ + public static String newPlayerResponseSignature(@NonNull String signature, boolean isShortAndOpeningOrPlaying) { + final boolean isShort = playerParametersAreShort(signature); + if (!isShort || isShortAndOpeningOrPlaying) { + if (videoIdIsShort != isShort) { + videoIdIsShort = isShort; + LogHelper.printDebug(() -> "videoIdIsShort: " + isShort); + } + } + return signature; // Return the original value since we are observing and not modifying. + } + /** * Injection point. Called off the main thread. * * @param videoId The id of the last video loaded. */ - public static void setPlayerResponseVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) { + public static void setPlayerResponseVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { if (!playerResponseVideoId.equals(videoId)) { LogHelper.printDebug(() -> "New player response video id: " + videoId); playerResponseVideoId = videoId; @@ -155,9 +181,9 @@ public static boolean seekToRelative(long millisecondsRelative) { } /** - * Id of the current video playing. Includes Shorts. + * Id of the last video opened. Includes Shorts. * - * @return The id of the video. Empty string if not set yet. + * @return The id of the video, or an empty string if no videos have been opened yet. */ @NonNull public static String getVideoId() { @@ -166,20 +192,30 @@ public static String getVideoId() { /** * Differs from {@link #videoId} as this is the video id for the - * last player response received, which may not be the current video playing. + * last player response received, which may not be the last video opened. *

* If Shorts are loading the background, this commonly will be * different from the Short that is currently on screen. *

* For most use cases, you should instead use {@link #getVideoId()}. * - * @return The id of the last video loaded. Empty string if not set yet. + * @return The id of the last video loaded, or an empty string if no videos have been loaded yet. */ @NonNull public static String getPlayerResponseVideoId() { return playerResponseVideoId; } + /** + * @return If the last player response video id _that was opened_ was a Short. + *

+ * Note: This value returned may not match the status of {@link #getPlayerResponseVideoId()} + * since that includes player responses for videos not opened. + */ + public static boolean lastVideoIdIsShort() { + return videoIdIsShort; + } + /** * @return The current playback speed. */ diff --git a/app/src/main/java/app/revanced/integrations/patches/components/ReturnYouTubeDislikeFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/ReturnYouTubeDislikeFilterPatch.java index 48c44f36ce..d6b7d151d7 100644 --- a/app/src/main/java/app/revanced/integrations/patches/components/ReturnYouTubeDislikeFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/components/ReturnYouTubeDislikeFilterPatch.java @@ -53,14 +53,14 @@ protected boolean removeEldestEntry(Map.Entry eldest) { /** * Injection point. */ - public static void newPlayerResponseVideoId(String videoId, boolean videoIsOpeningOrPlaying) { + public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) { try { - if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) { + if (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) { return; } synchronized (lastVideoIds) { if (lastVideoIds.put(videoId, Boolean.TRUE) == null) { - LogHelper.printDebug(() -> "New video id: " + videoId); + LogHelper.printDebug(() -> "New Short video id: " + videoId); } } } catch (Exception ex) { diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java index 93ca4c4d85..ed29e44d32 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java @@ -37,11 +37,6 @@ public class SpoofSignaturePatch { */ private static final String SCRIM_PARAMETER = "SAFgAXgB"; - /** - * Parameters used in YouTube Shorts. - */ - private static final String SHORTS_PLAYER_PARAMETERS = "8AEB"; - /** * Last video id loaded. Used to prevent reloading the same spec multiple times. */ @@ -62,7 +57,7 @@ public class SpoofSignaturePatch { * * @param parameters Original protobuf parameter value. */ - public static String spoofParameter(String parameters) { + public static String spoofParameter(String parameters, boolean isShortAndOpeningOrPlaying) { try { LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters); @@ -74,7 +69,7 @@ public static String spoofParameter(String parameters) { if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters; // Shorts do not need to be spoofed. - if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) { + if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) { isPlayingShorts = true; return parameters; } diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index e08b48db9d..1712d40a88 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -62,12 +62,12 @@ public class ReturnYouTubeDislikeApi { * How long to wait until API calls are resumed, if the API requested a back off. * No clear guideline of how long to wait until resuming. */ - private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes. + private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes. /** * How long to wait until API calls are resumed, if any connection error occurs. */ - private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 60 * 1000; // 60 Seconds. + private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes. /** * If non zero, then the system time of when API calls can resume. diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java index e269c9328b..5ee90efa71 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -13,7 +13,6 @@ import android.preference.SwitchPreference; import app.revanced.integrations.patches.ReturnYouTubeDislikePatch; -import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.settings.SettingsEnum; @@ -21,9 +20,6 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { - private static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER = - SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40"); - /** * If dislikes are shown on Shorts. */ @@ -79,7 +75,7 @@ public void onCreate(Bundle savedInstanceState) { shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean()); shortsPreference.setTitle(str("revanced_ryd_shorts_title")); String shortsSummary = str("revanced_ryd_shorts_summary_on", - IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER + ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER ? "" : "\n\n" + str("revanced_ryd_shorts_summary_disclaimer")); shortsPreference.setSummaryOn(shortsSummary); diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt index e2777c867c..b74a4d6329 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt @@ -1,10 +1,11 @@ package app.revanced.integrations.shared +import app.revanced.integrations.patches.VideoInformation import app.revanced.integrations.utils.Event import app.revanced.integrations.utils.LogHelper /** - * WatchWhile player type + * WatchWhile player type. */ enum class PlayerType { /** @@ -83,6 +84,8 @@ enum class PlayerType { * Does not include the first moment after a short is opened when a regular video is minimized on screen, * or while watching a short with a regular video present on a spoofed 16.x version of YouTube. * To include those situations instead use [isNoneHiddenOrMinimized]. + * + * @see VideoInformation */ fun isNoneOrHidden(): Boolean { return this == NONE || this == HIDDEN @@ -99,6 +102,7 @@ enum class PlayerType { * though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]). * * @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state. + * @see VideoInformation */ fun isNoneHiddenOrSlidingMinimized(): Boolean { return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED @@ -117,6 +121,7 @@ enum class PlayerType { * * @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state, * a regular video is minimized (and a new video is not being opened). + * @see VideoInformation */ fun isNoneHiddenOrMinimized(): Boolean { return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED