fix(YouTube - Return YouTube Dislike): Prevent the first Short opened from freezing the UI (#532)

This commit is contained in:
LisoUseInAIKyrios 2023-12-04 10:47:29 +02:00 committed by GitHub
parent d484f35127
commit 0bb86694e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 99 additions and 40 deletions

View File

@ -9,6 +9,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch; import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.shared.PlayerType;
@ -27,19 +28,25 @@ import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislik
* Handles all interaction of UI patch components. * Handles all interaction of UI patch components.
* *
* Known limitation: * 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: * A (yet to be implemented) solution that fixes this problem. Any one of:
* Enable app spoofing to version 18.33.40 or older, as that uses a non litho Shorts player. * - 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,
* Permanent fix (yet to be implemented), either of: * and use that hook to force the shorts dislikes to update after the fetch is completed.
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously. * - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
* - Find a way to force Litho to rebuild it's component tree * generated image of the number of dislikes, then update the image asynchronously. This Could
* (and use that hook to force the shorts dislikes to update after the fetch is completed). * 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") @SuppressWarnings("unused")
public class ReturnYouTubeDislikePatch { 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. * RYD data for the current video on screen.
*/ */
@ -549,26 +556,46 @@ public class ReturnYouTubeDislikePatch {
// Video Id and voting hooks (all players). // Video Id and voting hooks (all players).
// //
private static volatile boolean lastPlayerResponseWasShort;
/** /**
* Injection point. Uses 'playback response' video id hook to preload RYD. * 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 { try {
// Shorts shelf in home and subscription feed causes player response hook to be called, if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
// 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()) {
return;
}
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) {
return; return;
} }
if (videoId.equals(lastPrefetchedVideoId)) { if (videoId.equals(lastPrefetchedVideoId)) {
return; return;
} }
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; lastPrefetchedVideoId = videoId;
LogHelper.printDebug(() -> "Prefetching RYD for video: " + 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) { } catch (Exception ex) {
LogHelper.printException(() -> "preloadVideoId failure", ex); LogHelper.printException(() -> "preloadVideoId failure", ex);
} }

View File

@ -17,6 +17,10 @@ import java.util.Objects;
public final class VideoInformation { public final class VideoInformation {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
private static final String SEEK_METHOD_NAME = "seekTo"; 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<Object> playerControllerRef; private static WeakReference<Object> playerControllerRef;
private static Method seekMethod; private static Method seekMethod;
@ -28,6 +32,7 @@ public final class VideoInformation {
@NonNull @NonNull
private static volatile String playerResponseVideoId = ""; private static volatile String playerResponseVideoId = "";
private static volatile boolean videoIdIsShort;
/** /**
* The current playback speed * The current playback speed
@ -65,12 +70,33 @@ public final class VideoInformation {
} }
} }
/**
* @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. * Injection point. Called off the main thread.
* *
* @param videoId The id of the last video loaded. * @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)) { if (!playerResponseVideoId.equals(videoId)) {
LogHelper.printDebug(() -> "New player response video id: " + videoId); LogHelper.printDebug(() -> "New player response video id: " + videoId);
playerResponseVideoId = videoId; playerResponseVideoId = videoId;
@ -155,9 +181,9 @@ public final class VideoInformation {
} }
/** /**
* 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 @NonNull
public static String getVideoId() { public static String getVideoId() {
@ -166,20 +192,30 @@ public final class VideoInformation {
/** /**
* Differs from {@link #videoId} as this is the video id for the * 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.
* <p> * <p>
* If Shorts are loading the background, this commonly will be * If Shorts are loading the background, this commonly will be
* different from the Short that is currently on screen. * different from the Short that is currently on screen.
* <p> * <p>
* For most use cases, you should instead use {@link #getVideoId()}. * 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 @NonNull
public static String getPlayerResponseVideoId() { public static String getPlayerResponseVideoId() {
return playerResponseVideoId; return playerResponseVideoId;
} }
/**
* @return If the last player response video id _that was opened_ was a Short.
* <p>
* 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. * @return The current playback speed.
*/ */

View File

@ -53,14 +53,14 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
/** /**
* Injection point. * Injection point.
*/ */
public static void newPlayerResponseVideoId(String videoId, boolean videoIsOpeningOrPlaying) { public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
try { try {
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) { if (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) {
return; return;
} }
synchronized (lastVideoIds) { synchronized (lastVideoIds) {
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) { if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
LogHelper.printDebug(() -> "New video id: " + videoId); LogHelper.printDebug(() -> "New Short video id: " + videoId);
} }
} }
} catch (Exception ex) { } catch (Exception ex) {

View File

@ -37,11 +37,6 @@ public class SpoofSignaturePatch {
*/ */
private static final String SCRIM_PARAMETER = "SAFgAXgB"; 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. * 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. * @param parameters Original protobuf parameter value.
*/ */
public static String spoofParameter(String parameters) { public static String spoofParameter(String parameters, boolean isShortAndOpeningOrPlaying) {
try { try {
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters); LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
@ -74,7 +69,7 @@ public class SpoofSignaturePatch {
if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters; if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters;
// Shorts do not need to be spoofed. // Shorts do not need to be spoofed.
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) { if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
isPlayingShorts = true; isPlayingShorts = true;
return parameters; return parameters;
} }

View File

@ -62,12 +62,12 @@ public class ReturnYouTubeDislikeApi {
* How long to wait until API calls are resumed, if the API requested a back off. * 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. * 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. * 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. * If non zero, then the system time of when API calls can resume.

View File

@ -13,7 +13,6 @@ import android.preference.PreferenceScreen;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
import app.revanced.integrations.patches.ReturnYouTubeDislikePatch; import app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SettingsEnum;
@ -21,9 +20,6 @@ import app.revanced.integrations.settings.SharedPrefCategory;
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { 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. * If dislikes are shown on Shorts.
*/ */
@ -79,7 +75,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean()); shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean());
shortsPreference.setTitle(str("revanced_ryd_shorts_title")); shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
String shortsSummary = str("revanced_ryd_shorts_summary_on", 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")); : "\n\n" + str("revanced_ryd_shorts_summary_disclaimer"));
shortsPreference.setSummaryOn(shortsSummary); shortsPreference.setSummaryOn(shortsSummary);

View File

@ -1,10 +1,11 @@
package app.revanced.integrations.shared package app.revanced.integrations.shared
import app.revanced.integrations.patches.VideoInformation
import app.revanced.integrations.utils.Event import app.revanced.integrations.utils.Event
import app.revanced.integrations.utils.LogHelper import app.revanced.integrations.utils.LogHelper
/** /**
* WatchWhile player type * WatchWhile player type.
*/ */
enum class PlayerType { 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, * 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. * 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]. * To include those situations instead use [isNoneHiddenOrMinimized].
*
* @see VideoInformation
*/ */
fun isNoneOrHidden(): Boolean { fun isNoneOrHidden(): Boolean {
return this == NONE || this == HIDDEN return this == NONE || this == HIDDEN
@ -99,6 +102,7 @@ enum class PlayerType {
* though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]). * 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. * @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state.
* @see VideoInformation
*/ */
fun isNoneHiddenOrSlidingMinimized(): Boolean { fun isNoneHiddenOrSlidingMinimized(): Boolean {
return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED 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, * @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). * a regular video is minimized (and a new video is not being opened).
* @see VideoInformation
*/ */
fun isNoneHiddenOrMinimized(): Boolean { fun isNoneHiddenOrMinimized(): Boolean {
return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED