fix(YouTube - Client spoof): Fix low resolution precise seeking thumbnails (#513)

This commit is contained in:
LisoUseInAIKyrios 2023-11-11 20:22:41 +02:00 committed by GitHub
parent 4b8a9b9b13
commit 5ef20a8133
4 changed files with 99 additions and 27 deletions

View File

@ -3,6 +3,10 @@ package app.revanced.integrations.patches.spoof;
import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
import static app.revanced.integrations.utils.ReVancedUtils.containsAny; import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -51,11 +55,15 @@ public class SpoofSignaturePatch {
private static volatile Future<StoryboardRenderer> rendererFuture; private static volatile Future<StoryboardRenderer> rendererFuture;
private static volatile boolean useOriginalStoryboardRenderer;
private static volatile boolean isPlayingShorts;
@Nullable @Nullable
private static StoryboardRenderer getRenderer() { private static StoryboardRenderer getRenderer() {
if (rendererFuture != null) { if (rendererFuture != null) {
try { try {
return rendererFuture.get(5000, TimeUnit.MILLISECONDS); return rendererFuture.get(4000, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) { } catch (TimeoutException ex) {
LogHelper.printDebug(() -> "Could not get renderer (get timed out)"); LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
} catch (ExecutionException | InterruptedException ex) { } catch (ExecutionException | InterruptedException ex) {
@ -81,27 +89,38 @@ public class SpoofSignaturePatch {
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops) // Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
// For this reason, the player parameters of a clip are usually very long (150~300 characters). // For this reason, the player parameters of a clip are usually very long (150~300 characters).
// Clips are 60 seconds or less in length, so no spoofing. // Clips are 60 seconds or less in length, so no spoofing.
var isClip = parameters.length() > 150; if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters;
if (isClip) return parameters;
// Shorts do not need to be spoofed. // Shorts do not need to be spoofed.
if (parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) return parameters; if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
isPlayingShorts = true;
return parameters;
}
isPlayingShorts = false;
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL && containsAny(parameters, AUTOPLAY_PARAMETERS); boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
if (isPlayingFeed) return SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean() ? && containsAny(parameters, AUTOPLAY_PARAMETERS);
// Prepend the scrim parameter to mute videos in feed. if (isPlayingFeed) {
SCRIM_PARAMETER + INCOGNITO_PARAMETERS : if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
// In order to prevent videos that are auto-played in feed to be added to history, // Don't spoof the feed video playback. This will cause video playback issues,
// only spoof the parameter if the video is not playing in the feed. // but only if user continues watching for more than 1 minute.
// This will cause playback issues in the feed, but it's better than manipulating the history. return parameters;
parameters; }
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
fetchStoryboardRenderer();
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
}
fetchStoryboardRenderer(); fetchStoryboardRenderer();
return INCOGNITO_PARAMETERS; return INCOGNITO_PARAMETERS;
} }
private static void fetchStoryboardRenderer() { private static void fetchStoryboardRenderer() {
if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
lastPlayerResponseVideoId = null;
rendererFuture = null;
return;
}
String videoId = VideoInformation.getPlayerResponseVideoId(); String videoId = VideoInformation.getPlayerResponseVideoId();
if (!videoId.equals(lastPlayerResponseVideoId)) { if (!videoId.equals(lastPlayerResponseVideoId)) {
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
@ -115,11 +134,17 @@ public class SpoofSignaturePatch {
getRenderer(); getRenderer();
} }
/** private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
* Injection point. boolean returnNullIfLiveStream) {
*/ if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
public static boolean getSeekbarThumbnailOverrideValue() { StoryboardRenderer renderer = getRenderer();
return SettingsEnum.SPOOF_SIGNATURE.getBoolean(); if (renderer != null) {
if (returnNullIfLiveStream && renderer.isLiveStream()) return null;
return renderer.getSpec();
}
}
return originalStoryboardRendererSpec;
} }
/** /**
@ -128,19 +153,24 @@ public class SpoofSignaturePatch {
*/ */
@Nullable @Nullable
public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { return getStoryboardRendererSpec(originalStoryboardRendererSpec, false);
StoryboardRenderer renderer = getRenderer(); }
if (renderer != null) return renderer.getSpec();
}
return originalStoryboardRendererSpec; /**
* Injection point.
* Uses additional check to handle live streams.
* Called from background threads and from the main thread.
*/
@Nullable
public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) {
return getStoryboardRendererSpec(originalStoryboardRendererSpec, true);
} }
/** /**
* Injection point. * Injection point.
*/ */
public static int getRecommendedLevel(int originalLevel) { public static int getRecommendedLevel(int originalLevel) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = getRenderer(); StoryboardRenderer renderer = getRenderer();
if (renderer != null) { if (renderer != null) {
Integer recommendedLevel = renderer.getRecommendedLevel(); Integer recommendedLevel = renderer.getRecommendedLevel();
@ -150,4 +180,30 @@ public class SpoofSignaturePatch {
return originalLevel; return originalLevel;
} }
/**
* Injection point. Forces seekbar to be shown for paid videos or
* if {@link SettingsEnum#SPOOF_STORYBOARD_RENDERER} is not enabled.
*/
public static boolean getSeekbarThumbnailOverrideValue() {
return SettingsEnum.SPOOF_SIGNATURE.getBoolean();
}
/**
* Injection point.
*
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
*/
public static void seekbarImageViewCreated(ImageView view) {
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()
|| SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
return;
}
if (isPlayingShorts) return;
view.setVisibility(View.GONE);
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
ViewGroup parentLayout = (ViewGroup) view.getParent();
parentLayout.setPadding(0, 0, 0, 0);
}
} }

View File

@ -7,11 +7,13 @@ import org.jetbrains.annotations.NotNull;
public final class StoryboardRenderer { public final class StoryboardRenderer {
private final String spec; private final String spec;
private final boolean isLiveStream;
@Nullable @Nullable
private final Integer recommendedLevel; private final Integer recommendedLevel;
public StoryboardRenderer(String spec, @Nullable Integer recommendedLevel) { public StoryboardRenderer(String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) {
this.spec = spec; this.spec = spec;
this.isLiveStream = isLiveStream;
this.recommendedLevel = recommendedLevel; this.recommendedLevel = recommendedLevel;
} }
@ -20,6 +22,10 @@ public final class StoryboardRenderer {
return spec; return spec;
} }
public boolean isLiveStream() {
return isLiveStream;
}
/** /**
* @return Recommended image quality level, or NULL if no recommendation exists. * @return Recommended image quality level, or NULL if no recommendation exists.
*/ */
@ -32,7 +38,8 @@ public final class StoryboardRenderer {
@Override @Override
public String toString() { public String toString() {
return "StoryboardRenderer{" + return "StoryboardRenderer{" +
"spec='" + spec + '\'' + "isLiveStream=" + isLiveStream +
", spec='" + spec + '\'' +
", recommendedLevel=" + recommendedLevel + ", recommendedLevel=" + recommendedLevel +
'}'; '}';
} }

View File

@ -22,6 +22,7 @@ public class StoryboardRendererRequester {
@Nullable @Nullable
private static JSONObject fetchPlayerResponse(@NonNull String requestBody) { private static JSONObject fetchPlayerResponse(@NonNull String requestBody) {
final long startTime = System.currentTimeMillis();
try { try {
ReVancedUtils.verifyOffMainThread(); ReVancedUtils.verifyOffMainThread();
Objects.requireNonNull(requestBody); Objects.requireNonNull(requestBody);
@ -40,6 +41,8 @@ public class StoryboardRendererRequester {
LogHelper.printException(() -> "API timed out", ex); LogHelper.printException(() -> "API timed out", ex);
} catch (Exception ex) { } catch (Exception ex) {
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex); LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
} finally {
LogHelper.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
} }
return null; return null;
@ -72,14 +75,17 @@ public class StoryboardRendererRequester {
@Nullable @Nullable
private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) { private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) {
try { try {
LogHelper.printDebug(() -> "Parsing response: " + playerResponse);
final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
final String storyboardsRendererTag = storyboards.has("playerLiveStoryboardSpecRenderer") final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer");
final String storyboardsRendererTag = isLiveStream
? "playerLiveStoryboardSpecRenderer" ? "playerLiveStoryboardSpecRenderer"
: "playerStoryboardSpecRenderer"; : "playerStoryboardSpecRenderer";
final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
StoryboardRenderer renderer = new StoryboardRenderer( StoryboardRenderer renderer = new StoryboardRenderer(
rendererElement.getString("spec"), rendererElement.getString("spec"),
isLiveStream,
rendererElement.has("recommendedLevel") rendererElement.has("recommendedLevel")
? rendererElement.getInt("recommendedLevel") ? rendererElement.getInt("recommendedLevel")
: null : null

View File

@ -182,6 +182,9 @@ public enum SettingsEnum {
"revanced_spoof_signature_verification_enabled_user_dialog_message"), "revanced_spoof_signature_verification_enabled_user_dialog_message"),
SPOOF_SIGNATURE_IN_FEED("revanced_spoof_signature_in_feed_enabled", BOOLEAN, FALSE, false, SPOOF_SIGNATURE_IN_FEED("revanced_spoof_signature_in_feed_enabled", BOOLEAN, FALSE, false,
parents(SPOOF_SIGNATURE)), parents(SPOOF_SIGNATURE)),
SPOOF_STORYBOARD_RENDERER("revanced_spoof_storyboard", BOOLEAN, TRUE, true,
parents(SPOOF_SIGNATURE)),
SPOOF_DEVICE_DIMENSIONS("revanced_spoof_device_dimensions", BOOLEAN, FALSE, true), SPOOF_DEVICE_DIMENSIONS("revanced_spoof_device_dimensions", BOOLEAN, FALSE, true),
BYPASS_URL_REDIRECTS("revanced_bypass_url_redirects", BOOLEAN, TRUE), BYPASS_URL_REDIRECTS("revanced_bypass_url_redirects", BOOLEAN, TRUE),
ANNOUNCEMENTS("revanced_announcements", BOOLEAN, TRUE), ANNOUNCEMENTS("revanced_announcements", BOOLEAN, TRUE),