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 16a284d0..06534d03 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 @@ -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.utils.ReVancedUtils.containsAny; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + import androidx.annotation.Nullable; import java.util.concurrent.ExecutionException; @@ -51,11 +55,15 @@ public class SpoofSignaturePatch { private static volatile Future rendererFuture; + private static volatile boolean useOriginalStoryboardRenderer; + + private static volatile boolean isPlayingShorts; + @Nullable private static StoryboardRenderer getRenderer() { if (rendererFuture != null) { try { - return rendererFuture.get(5000, TimeUnit.MILLISECONDS); + return rendererFuture.get(4000, TimeUnit.MILLISECONDS); } catch (TimeoutException ex) { LogHelper.printDebug(() -> "Could not get renderer (get timed out)"); } 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) // 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. - var isClip = parameters.length() > 150; - if (isClip) return parameters; + if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters; // 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); - if (isPlayingFeed) return SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean() ? - // Prepend the scrim parameter to mute videos in feed. - SCRIM_PARAMETER + INCOGNITO_PARAMETERS : - // In order to prevent videos that are auto-played in feed to be added to history, - // only spoof the parameter if the video is not playing in the feed. - // This will cause playback issues in the feed, but it's better than manipulating the history. - parameters; + boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL + && containsAny(parameters, AUTOPLAY_PARAMETERS); + if (isPlayingFeed) { + if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) { + // Don't spoof the feed video playback. This will cause video playback issues, + // but only if user continues watching for more than 1 minute. + return parameters; + } + // Spoof the feed video. Video will show up in watch history and video subtitles are missing. + fetchStoryboardRenderer(); + return SCRIM_PARAMETER + INCOGNITO_PARAMETERS; + } fetchStoryboardRenderer(); - return INCOGNITO_PARAMETERS; } private static void fetchStoryboardRenderer() { + if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) { + lastPlayerResponseVideoId = null; + rendererFuture = null; + return; + } String videoId = VideoInformation.getPlayerResponseVideoId(); if (!videoId.equals(lastPlayerResponseVideoId)) { rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); @@ -115,11 +134,17 @@ public class SpoofSignaturePatch { getRenderer(); } - /** - * Injection point. - */ - public static boolean getSeekbarThumbnailOverrideValue() { - return SettingsEnum.SPOOF_SIGNATURE.getBoolean(); + private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec, + boolean returnNullIfLiveStream) { + if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) { + StoryboardRenderer renderer = getRenderer(); + if (renderer != null) { + if (returnNullIfLiveStream && renderer.isLiveStream()) return null; + return renderer.getSpec(); + } + } + + return originalStoryboardRendererSpec; } /** @@ -128,19 +153,24 @@ public class SpoofSignaturePatch { */ @Nullable public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { - if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { - StoryboardRenderer renderer = getRenderer(); - if (renderer != null) return renderer.getSpec(); - } + return getStoryboardRendererSpec(originalStoryboardRendererSpec, false); + } - 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. */ public static int getRecommendedLevel(int originalLevel) { - if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { + if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) { StoryboardRenderer renderer = getRenderer(); if (renderer != null) { Integer recommendedLevel = renderer.getRecommendedLevel(); @@ -150,4 +180,30 @@ public class SpoofSignaturePatch { 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); + } } diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java index d0e70988..32f5608c 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java @@ -7,11 +7,13 @@ import org.jetbrains.annotations.NotNull; public final class StoryboardRenderer { private final String spec; + private final boolean isLiveStream; @Nullable private final Integer recommendedLevel; - public StoryboardRenderer(String spec, @Nullable Integer recommendedLevel) { + public StoryboardRenderer(String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) { this.spec = spec; + this.isLiveStream = isLiveStream; this.recommendedLevel = recommendedLevel; } @@ -20,6 +22,10 @@ public final class StoryboardRenderer { return spec; } + public boolean isLiveStream() { + return isLiveStream; + } + /** * @return Recommended image quality level, or NULL if no recommendation exists. */ @@ -32,7 +38,8 @@ public final class StoryboardRenderer { @Override public String toString() { return "StoryboardRenderer{" + - "spec='" + spec + '\'' + + "isLiveStream=" + isLiveStream + + ", spec='" + spec + '\'' + ", recommendedLevel=" + recommendedLevel + '}'; } diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java index 61828a04..38904dc2 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java @@ -22,6 +22,7 @@ public class StoryboardRendererRequester { @Nullable private static JSONObject fetchPlayerResponse(@NonNull String requestBody) { + final long startTime = System.currentTimeMillis(); try { ReVancedUtils.verifyOffMainThread(); Objects.requireNonNull(requestBody); @@ -40,6 +41,8 @@ public class StoryboardRendererRequester { LogHelper.printException(() -> "API timed out", ex); } catch (Exception ex) { LogHelper.printException(() -> "Failed to fetch storyboard URL", ex); + } finally { + LogHelper.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms"); } return null; @@ -72,14 +75,17 @@ public class StoryboardRendererRequester { @Nullable private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) { try { + LogHelper.printDebug(() -> "Parsing response: " + playerResponse); final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); - final String storyboardsRendererTag = storyboards.has("playerLiveStoryboardSpecRenderer") + final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer"); + final String storyboardsRendererTag = isLiveStream ? "playerLiveStoryboardSpecRenderer" : "playerStoryboardSpecRenderer"; final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); StoryboardRenderer renderer = new StoryboardRenderer( rendererElement.getString("spec"), + isLiveStream, rendererElement.has("recommendedLevel") ? rendererElement.getInt("recommendedLevel") : null diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index 53a911e5..105bea54 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -182,6 +182,9 @@ public enum SettingsEnum { "revanced_spoof_signature_verification_enabled_user_dialog_message"), SPOOF_SIGNATURE_IN_FEED("revanced_spoof_signature_in_feed_enabled", BOOLEAN, FALSE, false, 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), BYPASS_URL_REDIRECTS("revanced_bypass_url_redirects", BOOLEAN, TRUE), ANNOUNCEMENTS("revanced_announcements", BOOLEAN, TRUE),