From 809970ae90025686c36113d28064b543122be9fe Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Sat, 31 Aug 2024 19:07:46 +0200 Subject: [PATCH] feat(YouTube): Remove `Spoof client` patch --- .../patches/misc/LiveStreamRenderer.java | 31 -- .../patches/misc/SpoofClientPatch.java | 523 ------------------ .../misc/SpoofPlayerParameterPatch.java | 220 -------- .../patches/misc/StoryboardRenderer.java | 38 -- .../requests/LiveStreamRendererRequester.java | 158 ------ .../patches/misc/requests/PlayerRoutes.java | 486 ---------------- .../requests/StoryboardRendererRequester.java | 184 ------ .../youtube/patches/utils/PatchStatus.java | 5 - .../youtube/settings/Settings.java | 16 - .../WatchHistoryStatusPreference.java | 21 +- 10 files changed, 6 insertions(+), 1676 deletions(-) delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/LiveStreamRenderer.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofClientPatch.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofPlayerParameterPatch.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/StoryboardRenderer.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/LiveStreamRendererRequester.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StoryboardRendererRequester.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/LiveStreamRenderer.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/LiveStreamRenderer.java deleted file mode 100644 index 5ff6098bc1..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/LiveStreamRenderer.java +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.integrations.youtube.patches.misc; - -import org.jetbrains.annotations.NotNull; - -/** - * @noinspection ALL - */ -public final class LiveStreamRenderer { - public final String videoId; - public final String client; - public final boolean playabilityOk; - public final boolean isLiveStream; - - public LiveStreamRenderer(String videoId, String client, boolean playabilityOk, boolean isLiveStream) { - this.videoId = videoId; - this.client = client; - this.playabilityOk = playabilityOk; - this.isLiveStream = isLiveStream; - } - - @NotNull - @Override - public String toString() { - return "LiveStreamRenderer{" + - "videoId=" + videoId + - ", client=" + client + - ", playabilityOk=" + playabilityOk + - ", isLiveStream=" + isLiveStream + - '}'; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofClientPatch.java deleted file mode 100644 index f3ba5f8baa..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofClientPatch.java +++ /dev/null @@ -1,523 +0,0 @@ -package app.revanced.integrations.youtube.patches.misc; - -import static app.revanced.integrations.shared.utils.Utils.submitOnBackgroundThread; - -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.apache.commons.lang3.StringUtils; - -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import app.revanced.integrations.shared.utils.Logger; -import app.revanced.integrations.youtube.patches.misc.requests.LiveStreamRendererRequester; -import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.ClientType; -import app.revanced.integrations.youtube.patches.misc.requests.StoryboardRendererRequester; -import app.revanced.integrations.youtube.settings.Settings; - -/** - * @noinspection ALL - */ -public class SpoofClientPatch { - private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); - - /** - * Clips or Shorts Parameters. - */ - private static final String[] CLIPS_OR_SHORTS_PARAMETERS = { - "kAIB", // Clips - "8AEB" // Shorts - }; - - /** - * iOS client is used for Clips or Shorts. - */ - private static volatile boolean isShortsOrClips; - - /** - * Any unreachable ip address. Used to intentionally fail requests. - */ - private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; - private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); - - /** - * Last spoofed client type. - */ - private static volatile ClientType lastSpoofedClientType; - - /** - * Last video id loaded. Used to prevent reloading the same spec multiple times. - */ - @NonNull - private static volatile String lastPlayerResponseVideoId = ""; - - @Nullable - private static volatile Future liveStreamRendererFuture; - - @Nullable - private static volatile Future storyboardRendererFuture; - - /** - * Injection point. - * Blocks /get_watch requests by returning a localhost URI. - * - * @param playerRequestUri The URI of the player request. - * @return Localhost URI if the request is a /get_watch request, otherwise the original URI. - */ - public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT_ENABLED) { - try { - String path = playerRequestUri.getPath(); - - if (path != null && path.contains("get_watch")) { - Logger.printDebug(() -> "Blocking: " + playerRequestUri + " by returning: " + UNREACHABLE_HOST_URI_STRING); - - return UNREACHABLE_HOST_URI; - } - } catch (Exception ex) { - Logger.printException(() -> "blockGetWatchRequest failure", ex); - } - } - - return playerRequestUri; - } - - /** - * Injection point. - *

- * Blocks /initplayback requests. - */ - public static String blockInitPlaybackRequest(String originalUrlString) { - if (SPOOF_CLIENT_ENABLED) { - try { - Uri originalUri = Uri.parse(originalUrlString); - String path = originalUri.getPath(); - - if (path != null && path.contains("initplayback")) { - String replacementUriString = (getSpoofClientType() != ClientType.ANDROID_TESTSUITE) - ? UNREACHABLE_HOST_URI_STRING - // TODO: Ideally, a local proxy could be setup and block - // the request the same way as Burp Suite is capable of - // because that way the request is never sent to YouTube unnecessarily. - // Just using localhost unfortunately does not work. - : originalUri.buildUpon().clearQuery().build().toString(); - - Logger.printDebug(() -> "Blocking: " + originalUrlString + " by returning: " + replacementUriString); - - return replacementUriString; - } - } catch (Exception ex) { - Logger.printException(() -> "blockInitPlaybackRequest failure", ex); - } - } - - return originalUrlString; - } - - private static ClientType getSpoofClientType() { - if (isShortsOrClips) { - lastSpoofedClientType = Settings.SPOOF_CLIENT_SHORTS.get(); - return lastSpoofedClientType; - } - LiveStreamRenderer renderer = getLiveStreamRenderer(false); - if (renderer != null) { - if (renderer.isLiveStream) { - lastSpoofedClientType = Settings.SPOOF_CLIENT_LIVESTREAM.get(); - return lastSpoofedClientType; - } - if (!renderer.playabilityOk) { - lastSpoofedClientType = Settings.SPOOF_CLIENT_FALLBACK.get(); - return lastSpoofedClientType; - } - } - lastSpoofedClientType = Settings.SPOOF_CLIENT_GENERAL.get(); - return lastSpoofedClientType; - } - - /** - * Injection point. - */ - public static int getClientTypeId(int originalClientTypeId) { - if (SPOOF_CLIENT_ENABLED) { - return getSpoofClientType().id; - } - - return originalClientTypeId; - } - - /** - * Injection point. - */ - public static String getClientVersion(String originalClientVersion) { - if (SPOOF_CLIENT_ENABLED) { - return getSpoofClientType().appVersion; - } - - return originalClientVersion; - } - - /** - * Injection point. - */ - public static String getClientModel(String originalClientModel) { - if (SPOOF_CLIENT_ENABLED) { - return getSpoofClientType().model; - } - - return originalClientModel; - } - - /** - * Injection point. - */ - public static String getOsVersion(String originalOsVersion) { - if (SPOOF_CLIENT_ENABLED) { - return getSpoofClientType().osVersion; - } - - return originalOsVersion; - } - - /** - * Injection point. - */ - public static String getUserAgent(String originalUserAgent) { - if (SPOOF_CLIENT_ENABLED) { - ClientType clientType = getSpoofClientType(); - if (clientType == ClientType.IOS) { - Logger.printDebug(() -> "Replaced: '" + originalUserAgent + "' with: '" - + clientType.userAgent + "'"); - return clientType.userAgent; - } - } - - return originalUserAgent; - } - - /** - * Injection point. - */ - public static boolean isClientSpoofingEnabled() { - return SPOOF_CLIENT_ENABLED; - } - - /** - * Injection point. - */ - public static boolean enablePlayerGesture(boolean original) { - return SPOOF_CLIENT_ENABLED || original; - } - - /** - * Injection point. - * When spoofing the client to iOS or Android Testsuite the playback speed menu is missing from the player response. - * Return true to force create the playback speed menu. - */ - public static boolean forceCreatePlaybackSpeedMenu(boolean original) { - if (SPOOF_CLIENT_ENABLED) { - return true; - } - - return original; - } - - /** - * Injection point. - * When spoofing the client to iOS, background audio only playback of livestreams fails. - * Return true to force enable audio background play. - */ - public static boolean overrideBackgroundAudioPlayback() { - return SPOOF_CLIENT_ENABLED && - BackgroundPlaybackPatch.playbackIsNotShort(); - } - - /** - * Injection point. - * When spoofing the client to Android TV the playback speed menu is missing from the player response. - * Return false to force create the playback speed menu. - */ - public static boolean forceCreatePlaybackSpeedMenuReversed(boolean original) { - if (SPOOF_CLIENT_ENABLED) { - return false; - } - - return original; - } - - private static final Uri VIDEO_STATS_PLAYBACK_URI = Uri.parse("https://www.youtube.com/api/stats/playback?ns=yt&ver=2&final=1"); - - private static final String PARAM_DOC_ID = "docid"; - private static final String PARAM_LEN = "len"; - private static final String PARAM_CPN = "cpn"; - - private static final String PARAM_EVENT_ID = "ei"; - private static final String PARAM_VM = "vm"; - private static final String PARAM_OF = "of"; - - private static String mDocId; - private static String mLen; - private static String mCpn; - private static String mEventId; - private static String mVisitorMonitoringData; - private static String mOfParam; - - /** - * Injection point. - */ - public static void setCpn(String cpn) { - if (SPOOF_CLIENT_ENABLED && !Objects.equals(mCpn, cpn)) { - mCpn = cpn; - } - } - - /** - * Injection point. - * - * Parse parameters from the Tracking URL. - * See yuliskov/MediaServiceCore. - */ - public static void setTrackingUriParameter(Uri trackingUri) { - try { - if (SPOOF_CLIENT_ENABLED) { - String path = trackingUri.getPath(); - - if (path == null || (!path.contains("playback") && !path.contains("watchtime"))) { - return; - } - - mDocId = getQueryParameter(trackingUri, PARAM_DOC_ID); - mLen = getQueryParameter(trackingUri, PARAM_LEN); - mEventId = getQueryParameter(trackingUri, PARAM_EVENT_ID); - mVisitorMonitoringData = getQueryParameter(trackingUri, PARAM_VM); - mOfParam = getQueryParameter(trackingUri, PARAM_OF); - - Logger.printDebug(() -> "docId: " + mDocId + ", len: " + mLen + ", eventId: " + mEventId + ", visitorMonitoringData: " + mVisitorMonitoringData + ", of: " + mOfParam); - } - } catch (Exception ex) { - Logger.printException(() -> "setTrackingUriParameter failure", ex); - } - } - - /** - * Injection point. - * This only works on YouTube 18.38.45 or earlier. - * - * Build a Tracking URL. - * This does not include the last watched time. - * See yuliskov/MediaServiceCore. - */ - public static Uri overrideTrackingUrl(Uri trackingUrl) { - try { - if (SPOOF_CLIENT_ENABLED && - getSpoofClientType() == ClientType.IOS && - trackingUrl.toString().contains("youtube.com/csi") && - !StringUtils.isAnyEmpty(mDocId, mLen, mCpn, mEventId, mVisitorMonitoringData, mOfParam) - ) { - final Uri videoStatsPlaybackUri = VIDEO_STATS_PLAYBACK_URI - .buildUpon() - .appendQueryParameter(PARAM_DOC_ID, mDocId) - .appendQueryParameter(PARAM_LEN, mLen) - .appendQueryParameter(PARAM_CPN, mCpn) - .appendQueryParameter(PARAM_EVENT_ID, mEventId) - .appendQueryParameter(PARAM_VM, mVisitorMonitoringData) - .appendQueryParameter(PARAM_OF, mOfParam) - .build(); - - Logger.printDebug(() -> "Replaced: '" + trackingUrl + "' with: '" + videoStatsPlaybackUri + "'"); - return videoStatsPlaybackUri; - } - } catch (Exception ex) { - Logger.printException(() -> "overrideTrackingUrl failure", ex); - } - - return trackingUrl; - } - - private static String getQueryParameter(Uri uri, String key) { - List queryParams = uri.getQueryParameters(key); - if (queryParams == null || queryParams.isEmpty()) { - return ""; - } else { - return queryParams.get(0); - } - } - - /** - * Injection point. - */ - public static String appendSpoofedClient(String videoFormat) { - try { - if (SPOOF_CLIENT_ENABLED && Settings.SPOOF_CLIENT_STATS_FOR_NERDS.get() - && !TextUtils.isEmpty(videoFormat)) { - // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages - return "\u202D" + videoFormat + String.format("\u2009(%s)", lastSpoofedClientType.friendlyName); // u202D = left to right override - } - } catch (Exception ex) { - Logger.printException(() -> "appendSpoofedClient failure", ex); - } - - return videoFormat; - } - - /** - * Injection point. - */ - public static String setPlayerResponseVideoId(@NonNull String videoId, @Nullable String parameters, boolean isShortAndOpeningOrPlaying) { - if (SPOOF_CLIENT_ENABLED) { - isShortsOrClips = playerParameterIsClipsOrShorts(parameters, isShortAndOpeningOrPlaying); - - if (!isShortsOrClips) { - fetchPlayerResponseRenderer(videoId, Settings.SPOOF_CLIENT_GENERAL.get()); - } - } - - return parameters; // Return the original value since we are observing and not modifying. - } - - /** - * @return If the player parameters are for a Short or Clips. - */ - private static boolean playerParameterIsClipsOrShorts(@Nullable String playerParameter, boolean isShortAndOpeningOrPlaying) { - if (isShortAndOpeningOrPlaying) { - return true; - } - - return playerParameter != null && StringUtils.startsWithAny(playerParameter, CLIPS_OR_SHORTS_PARAMETERS); - } - - private static void fetchPlayerResponseRenderer(@NonNull String videoId, @NonNull ClientType clientType) { - if (!videoId.equals(lastPlayerResponseVideoId)) { - lastPlayerResponseVideoId = videoId; - liveStreamRendererFuture = submitOnBackgroundThread(() -> LiveStreamRendererRequester.getLiveStreamRenderer(videoId, clientType)); - storyboardRendererFuture = submitOnBackgroundThread(() -> StoryboardRendererRequester.getStoryboardRenderer(videoId)); - } - // Block until the renderer fetch completes. - // This is desired because if this returns without finishing the fetch - // then video will start playback but the storyboard is not ready yet. - getLiveStreamRenderer(true); - } - - @Nullable - private static LiveStreamRenderer getLiveStreamRenderer(boolean waitForCompletion) { - Future future = liveStreamRendererFuture; - if (future != null) { - try { - if (waitForCompletion || future.isDone()) { - return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout. - } // else, return null. - } catch (TimeoutException ex) { - Logger.printDebug(() -> "Could not get renderer (get timed out)"); - } catch (ExecutionException | InterruptedException ex) { - // Should never happen. - Logger.printException(() -> "Could not get renderer", ex); - } - } - return null; - } - - @Nullable - private static StoryboardRenderer getStoryboardRenderer(boolean waitForCompletion) { - Future future = storyboardRendererFuture; - if (future != null) { - try { - if (waitForCompletion || future.isDone()) { - return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout. - } // else, return null. - } catch (TimeoutException ex) { - Logger.printDebug(() -> "Could not get renderer (get timed out)"); - } catch (ExecutionException | InterruptedException ex) { - // Should never happen. - Logger.printException(() -> "Could not get renderer", ex); - } - } - return null; - } - - private static boolean useFetchedStoryboardRenderer() { - if (!SPOOF_CLIENT_ENABLED || isShortsOrClips) { - return false; - } - - // No seekbar thumbnail or low quality seekbar thumbnail. - final ClientType clientType = getSpoofClientType(); - return clientType == ClientType.ANDROID_TESTSUITE || clientType == ClientType.ANDROID_UNPLUGGED; - } - - private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec, - boolean returnNullIfLiveStream) { - if (useFetchedStoryboardRenderer()) { - final StoryboardRenderer renderer = getStoryboardRenderer(false); - if (renderer != null) { - if (returnNullIfLiveStream && renderer.isLiveStream) { - return null; - } - String spec = renderer.spec; - if (spec != null) { - return spec; - } - } - } - - return originalStoryboardRendererSpec; - } - - /** - * Injection point. - * Called from background threads and from the main thread. - */ - @Nullable - public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { - return getStoryboardRendererSpec(originalStoryboardRendererSpec, false); - } - - /** - * 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 getStoryboardRecommendedLevel(int originalLevel) { - if (useFetchedStoryboardRenderer()) { - final StoryboardRenderer renderer = getStoryboardRenderer(false); - if (renderer != null) { - Integer recommendedLevel = renderer.recommendedLevel; - if (recommendedLevel != null) return recommendedLevel; - } - } - - return originalLevel; - } - - /** - * Injection point. Forces seekbar to be shown for paid videos or - * if {@link Settings#SPOOF_PLAYER_PARAMETER} is not enabled. - */ - public static boolean getSeekbarThumbnailOverrideValue() { - if (!useFetchedStoryboardRenderer()) { - return false; - } - final StoryboardRenderer renderer = getStoryboardRenderer(false); - if (renderer == null) { - // Spoof storyboard renderer is turned off, - // video is paid, or the storyboard fetch timed out. - // Show empty thumbnails so the seek time and chapters still show up. - return true; - } - return renderer.spec != null; - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofPlayerParameterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofPlayerParameterPatch.java deleted file mode 100644 index 153ac4efb1..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofPlayerParameterPatch.java +++ /dev/null @@ -1,220 +0,0 @@ -package app.revanced.integrations.youtube.patches.misc; - -import static app.revanced.integrations.shared.utils.Utils.containsAny; -import static app.revanced.integrations.shared.utils.Utils.submitOnBackgroundThread; -import static app.revanced.integrations.youtube.patches.misc.requests.StoryboardRendererRequester.getStoryboardRenderer; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import app.revanced.integrations.shared.utils.Logger; -import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.youtube.shared.PlayerType; -import app.revanced.integrations.youtube.shared.VideoInformation; - -/** - * @noinspection ALL - *

- * Even if user spoof any player parameters with the client name "ANDROID", if a valid DroidGuard result is not sent, - * user always receive a response with video id 'aQvGIIdgFDM' (the following content is not available on this app). - * YouTube.js#623 - * Therefore, this patch is no longer valid. - *

- * Currently, the only client name available on Android without DroidGuard results is "ANDROID_TESTSUITE". - * invidious#4650 - */ -@Deprecated -public class SpoofPlayerParameterPatch { - private static final boolean spoofParameter = Settings.SPOOF_PLAYER_PARAMETER.get(); - private static final boolean spoofParameterInFeed = Settings.SPOOF_PLAYER_PARAMETER_IN_FEED.get(); - - /** - * Parameter (also used by - * YouTube.js) - * to fix playback issues. - */ - private static final String INCOGNITO_PARAMETERS = "CgIIAQ%3D%3D"; - - /** - * Parameters used when playing clips. - */ - private static final String CLIPS_PARAMETERS = "kAIB"; - - /** - * Parameters causing playback issues. - */ - private static final String[] AUTOPLAY_PARAMETERS = { - "YAHI", // Autoplay in feed. - "SAFg" // Autoplay in scrim. - }; - - /** - * Parameter used for autoplay in scrim. - * Prepend this parameter to mute video playback (for autoplay in feed). - */ - private static final String SCRIM_PARAMETER = "SAFgAXgB"; - - /** - * Last video id loaded. Used to prevent reloading the same spec multiple times. - */ - @Nullable - private static volatile String lastPlayerResponseVideoId; - - @Nullable - private static volatile Future rendererFuture; - - private static volatile boolean useOriginalStoryboardRenderer; - - @Nullable - private static StoryboardRenderer getRenderer(boolean waitForCompletion) { - Future future = rendererFuture; - if (future != null) { - try { - if (waitForCompletion || future.isDone()) { - return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout. - } // else, return null. - } catch (TimeoutException ex) { - Logger.printDebug(() -> "Could not get renderer (get timed out)"); - } catch (ExecutionException | InterruptedException ex) { - // Should never happen. - Logger.printException(() -> "Could not get renderer", ex); - } - } - return null; - } - - /** - * Injection point. - *

- * {@link VideoInformation#getVideoId()} cannot be used because it is injected after PlayerResponse. - * Therefore, we use the videoId called from PlaybackStartDescriptor. - * - * @param videoId Original video id value. - * @param parameters Original player parameter value. - */ - public static String spoofParameter(@NonNull String videoId, @Nullable String parameters, boolean isShortAndOpeningOrPlaying) { - try { - Logger.printDebug(() -> "Original player parameter value: " + parameters); - - if (!spoofParameter) { - return parameters; - } - - // Shorts do not need to be spoofed. - if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) { - return parameters; - } - - // Clip's player parameters contain important information such as where the video starts, where it ends, and whether it loops. - // Clips are 60 seconds or less in length, so no spoofing. - if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) { - return parameters; - } - - final boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL - && containsAny(parameters, AUTOPLAY_PARAMETERS); - if (isPlayingFeed) { - //noinspection AssignmentUsedAsCondition - if (useOriginalStoryboardRenderer = !spoofParameterInFeed) { - // 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(videoId); - return SCRIM_PARAMETER + INCOGNITO_PARAMETERS; - } - - fetchStoryboardRenderer(videoId); - } catch (Exception ex) { - Logger.printException(() -> "spoofParameter failure", ex); - } - return INCOGNITO_PARAMETERS; - } - - private static void fetchStoryboardRenderer(@NonNull String videoId) { - if (!videoId.equals(lastPlayerResponseVideoId)) { - rendererFuture = submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); - lastPlayerResponseVideoId = videoId; - } - // Block until the renderer fetch completes. - // This is desired because if this returns without finishing the fetch - // then video will start playback but the storyboard is not ready yet. - getRenderer(true); - } - - private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec, - boolean returnNullIfLiveStream) { - if (spoofParameter && !useOriginalStoryboardRenderer) { - final StoryboardRenderer renderer = getRenderer(false); - if (renderer != null) { - if (returnNullIfLiveStream && renderer.isLiveStream) { - return null; - } - String spec = renderer.spec; - if (spec != null) { - return spec; - } - } - } - - return originalStoryboardRendererSpec; - } - - /** - * Injection point. - * Called from background threads and from the main thread. - */ - @Nullable - public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { - return getStoryboardRendererSpec(originalStoryboardRendererSpec, false); - } - - /** - * 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 getStoryboardRecommendedLevel(int originalLevel) { - if (spoofParameter && !useOriginalStoryboardRenderer) { - final StoryboardRenderer renderer = getRenderer(false); - if (renderer != null) { - Integer recommendedLevel = renderer.recommendedLevel; - if (recommendedLevel != null) return recommendedLevel; - } - } - - return originalLevel; - } - - /** - * Injection point. Forces seekbar to be shown for paid videos or - * if {@link Settings#SPOOF_PLAYER_PARAMETER} is not enabled. - */ - public static boolean getSeekbarThumbnailOverrideValue() { - if (!spoofParameter) { - return false; - } - final StoryboardRenderer renderer = getRenderer(false); - if (renderer == null) { - // Spoof storyboard renderer is turned off, - // video is paid, or the storyboard fetch timed out. - // Show empty thumbnails so the seek time and chapters still show up. - return true; - } - return renderer.spec != null; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/StoryboardRenderer.java deleted file mode 100644 index e5b626312c..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/StoryboardRenderer.java +++ /dev/null @@ -1,38 +0,0 @@ -package app.revanced.integrations.youtube.patches.misc; - -import androidx.annotation.Nullable; - -import org.jetbrains.annotations.NotNull; - -/** - * @noinspection ALL - */ -public final class StoryboardRenderer { - public final String videoId; - @Nullable - public final String spec; - public final boolean isLiveStream; - /** - * Recommended image quality level, or NULL if no recommendation exists. - */ - @Nullable - public final Integer recommendedLevel; - - public StoryboardRenderer(String videoId, @Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) { - this.videoId = videoId; - this.spec = spec; - this.isLiveStream = isLiveStream; - this.recommendedLevel = recommendedLevel; - } - - @NotNull - @Override - public String toString() { - return "StoryboardRenderer{" + - "videoId=" + videoId + - ", isLiveStream=" + isLiveStream + - ", spec='" + spec + '\'' + - ", recommendedLevel=" + recommendedLevel + - '}'; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/LiveStreamRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/LiveStreamRendererRequester.java deleted file mode 100644 index eba2749fdf..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/LiveStreamRendererRequester.java +++ /dev/null @@ -1,158 +0,0 @@ -package app.revanced.integrations.youtube.patches.misc.requests; - -import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_LIVE_STREAM_RENDERER; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; - -import app.revanced.integrations.shared.requests.Requester; -import app.revanced.integrations.shared.utils.Logger; -import app.revanced.integrations.shared.utils.Utils; -import app.revanced.integrations.youtube.patches.misc.LiveStreamRenderer; -import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.ClientType; - -public class LiveStreamRendererRequester { - - private LiveStreamRendererRequester() { - } - - private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static JSONObject fetchPlayerResponse(@NonNull String requestBody, - @NonNull String userAgent) { - final long startTime = System.currentTimeMillis(); - try { - Utils.verifyOffMainThread(); - Objects.requireNonNull(requestBody); - - final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); - - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_LIVE_STREAM_RENDERER, userAgent); - connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length); - - final int responseCode = connection.getResponseCode(); - if (responseCode == 200) return Requester.parseJSONObject(connection); - - // Always show a toast for this, as a non 200 response means something is broken. - handleConnectionError("Fetch livestreams not available: " + responseCode, null); - connection.disconnect(); - } catch (SocketTimeoutException ex) { - handleConnectionError("Fetch livestreams temporarily not available (API timed out)", ex); - } catch (IOException ex) { - handleConnectionError("Fetch livestreams temporarily not available: " + ex.getMessage(), ex); - } catch (Exception ex) { - Logger.printException(() -> "Fetch livestreams failed", ex); // Should never happen. - } finally { - Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) { - try { - return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); - } catch (JSONException e) { - Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); - } - - return false; - } - - private static boolean isLive(@NonNull JSONObject playerResponse) { - try { - return playerResponse.getJSONObject("videoDetails").getBoolean("isLive"); - } catch (JSONException e) { - Logger.printDebug(() -> "Failed to get videoDetails for response: " + playerResponse); - } - - return false; - } - - private static boolean isLiveContent(@NonNull JSONObject playerResponse) { - try { - return playerResponse.getJSONObject("videoDetails").getBoolean("isLiveContent"); - } catch (JSONException e) { - Logger.printDebug(() -> "Failed to get videoDetails for response: " + playerResponse); - } - - return false; - } - - /** - * Fetches the liveStreamRenderer from the innerTubeBody. - * - * @return LiveStreamRenderer or null if playabilityStatus is not OK. - */ - @Nullable - private static LiveStreamRenderer getLiveStreamRendererUsingBody(@NonNull String videoId, - @NonNull ClientType clientType) { - final JSONObject playerResponse = fetchPlayerResponse( - String.format(clientType.innerTubeBody, videoId), - clientType.userAgent - ); - if (playerResponse != null) - return getLiveStreamRendererUsingResponse(videoId, playerResponse, clientType); - - return null; - } - - @Nullable - private static LiveStreamRenderer getLiveStreamRendererUsingResponse(@NonNull String videoId, - @NonNull JSONObject playerResponse, - @NonNull ClientType clientType) { - try { - Logger.printDebug(() -> "Parsing liveStreamRenderer from response: " + playerResponse); - - final String clientName = clientType.name(); - final boolean isPlayabilityOk = isPlayabilityStatusOk(playerResponse); - final boolean isLiveStream = isLive(playerResponse) || isLiveContent(playerResponse); - - LiveStreamRenderer renderer = new LiveStreamRenderer( - videoId, - clientName, - isPlayabilityOk, - isLiveStream - ); - Logger.printDebug(() -> "Fetched: " + renderer); - - return renderer; - } catch (Exception e) { - Logger.printException(() -> "Failed to get liveStreamRenderer", e); - } - - return null; - } - - @Nullable - public static LiveStreamRenderer getLiveStreamRenderer(@NonNull String videoId, @NonNull ClientType clientType) { - Objects.requireNonNull(videoId); - - LiveStreamRenderer renderer = getLiveStreamRendererUsingBody(videoId, clientType); - if (renderer == null) { - String finalClientName1 = clientType.name(); - Logger.printDebug(() -> videoId + " not available using " + finalClientName1 + " client"); - - clientType = ClientType.TVHTML5_SIMPLY_EMBEDDED_PLAYER; - renderer = getLiveStreamRendererUsingBody(videoId, clientType); - if (renderer == null) { - String finalClientName2 = clientType.name(); - Logger.printDebug(() -> videoId + " not available using " + finalClientName2 + " client"); - } - } - - return renderer; - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java deleted file mode 100644 index 2f20413c4b..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java +++ /dev/null @@ -1,486 +0,0 @@ -package app.revanced.integrations.youtube.patches.misc.requests; - -import static app.revanced.integrations.shared.utils.StringRef.str; - -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.os.Build; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.HttpURLConnection; - -import app.revanced.integrations.shared.requests.Requester; -import app.revanced.integrations.shared.requests.Route; -import app.revanced.integrations.shared.settings.Setting; -import app.revanced.integrations.shared.utils.Logger; -import app.revanced.integrations.shared.utils.PackageUtils; -import app.revanced.integrations.youtube.settings.Settings; - -public final class PlayerRoutes { - public static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route( - Route.Method.POST, - "player" + - "?fields=storyboards.playerStoryboardSpecRenderer," + - "storyboards.playerLiveStoryboardSpecRenderer," + - "playabilityStatus.status," + - "playabilityStatus.errorScreen" - ).compile(); - - public static final Route.CompiledRoute GET_LIVE_STREAM_RENDERER = new Route( - Route.Method.POST, - "player" + - "?fields=playabilityStatus.status," + - "videoDetails.isLive," + - "videoDetails.isLiveContent" - ).compile(); - - - private static final String ANDROID_CLIENT_VERSION = PackageUtils.getVersionName(); - private static final String ANDROID_DEVICE_MODEL = Build.MODEL; - private static final String ANDROID_OS_RELEASE_VERSION = Build.VERSION.RELEASE; - private static final int ANDROID_OS_SDK_VERSION = Build.VERSION.SDK_INT; - private static final String ANDROID_USER_AGENT = "com.google.android.youtube/" + - ANDROID_CLIENT_VERSION + - " (Linux; U; Android " + - ANDROID_OS_RELEASE_VERSION + - "; GB) gzip"; - - private static final String ANDROID_TESTSUITE_CLIENT_VERSION = "1.9"; - - - private static final String ANDROID_UNPLUGGED_CLIENT_VERSION = "8.31.0"; - /** - * The device machine id for the Chromecast with Google TV 4K. - * - *

- * See this GitLab for more - * information. - *

- */ - private static final String ANDROID_UNPLUGGED_DEVICE_MODEL = "Chromecast"; - private static final String ANDROID_UNPLUGGED_OS_RELEASE_VERSION = "12"; - private static final int ANDROID_UNPLUGGED_OS_SDK_VERSION = 31; - private static final String ANDROID_UNPLUGGED_USER_AGENT = "com.google.android.apps.youtube.unplugged/" + - ANDROID_UNPLUGGED_CLIENT_VERSION + - " (Linux; U; Android " + - ANDROID_UNPLUGGED_OS_RELEASE_VERSION + - "; GB) gzip"; - - - /** - * The hardcoded client version of the Android VR app used for InnerTube requests with this client. - * - *

- * It can be extracted by getting the latest release version of the app on - * the App - * Store page of the YouTube app, in the {@code Additional details} section. - *

- */ - private static final String ANDROID_VR_CLIENT_VERSION = "1.58.14"; - - /** - * The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client. - * - *

- * See this GitLab for more - * information. - *

- */ - private static final String ANDROID_VR_DEVICE_MODEL = "Quest 3"; - - private static final String ANDROID_VR_OS_RELEASE_VERSION = "12"; - /** - * The SDK version for Android 12 is 31, - * but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32. - */ - private static final int ANDROID_VR_OS_SDK_VERSION = 32; - - /** - * Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated) - * Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus - * Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico - */ - private static final String ANDROID_VR_USER_AGENT = "com.google.android.apps.youtube.vr.oculus/" + - ANDROID_VR_CLIENT_VERSION + - " (Linux; U; Android 12; GB) gzip"; - - - /** - * The hardcoded client version of the iOS app used for InnerTube requests with this client. - * - *

- * It can be extracted by getting the latest release version of the app on - * the App - * Store page of the YouTube app, in the {@code What’s New} section. - *

- */ - private static final String IOS_CLIENT_VERSION = "19.16.3"; - /** - * The device machine id for the iPhone XS Max (iPhone11,4), used to get 60fps. - * The device machine id for the iPhone 15 Pro Max (iPhone16,2), used to get HDR with AV1 hardware decoding. - * - *

- * See this GitHub Gist for more - * information. - *

- */ - private static final String IOS_DEVICE_MODEL = DeviceHardwareSupport.allowAV1() - ? "iPhone16,2" - : "iPhone11,4"; - - /** - * The minimum supported OS version for the iOS YouTube client is iOS 14.0. - * Using an invalid OS version will use the AVC codec. - */ - private static final String IOS_OS_VERSION = DeviceHardwareSupport.allowVP9() - ? "17.6.1.21G101" - : "13.7.17H35"; - private static final String IOS_USER_AGENT_VERSION = DeviceHardwareSupport.allowVP9() - ? "17_6_1" - : "13_7"; - private static final String IOS_USER_AGENT = "com.google.ios.youtube/" + - IOS_CLIENT_VERSION + - "(" + - IOS_DEVICE_MODEL + - "; U; CPU iOS " + - IOS_USER_AGENT_VERSION + - " like Mac OS X)"; - - private static final String TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION = "2.0"; - private static final String TVHTML5_SIMPLY_EMBEDDED_PLAYER_USER_AGENT = "Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5)" + - " AppleWebKit/537.36 (KHTML, like Gecko)" + - " 85.0.4183.93/6.5 TV Safari/537.36"; - private static final String WEB_CLIENT_VERSION = "2.20240726.00.00"; - private static final String WEB_OS_VERSION = "10"; - private static final String WEB_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" + - " AppleWebKit/537.36 (KHTML, like Gecko)" + - " Chrome/124.0.0.0 Mobile Safari/537.36"; - - private static final String ANDROID_INNER_TUBE_BODY; - private static final String ANDROID_EMBED_INNER_TUBE_BODY; - private static final String ANDROID_TESTSUITE_INNER_TUBE_BODY; - private static final String ANDROID_UNPLUGGED_INNER_TUBE_BODY; - private static final String ANDROID_VR_INNER_TUBE_BODY; - private static final String IOS_INNER_TUBE_BODY; - private static final String TVHTML5_SIMPLY_EMBED_INNER_TUBE_BODY; - private static final String WEB_INNER_TUBE_BODY; - - private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/"; - - /** - * TCP connection and HTTP read timeout - */ - private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. - - static { - JSONObject androidInnerTubeBody = new JSONObject(); - JSONObject androidEmbedInnerTubeBody = new JSONObject(); - JSONObject androidTestsuiteInnerTubeBody = new JSONObject(); - JSONObject androidUnpluggedInnerTubeBody = new JSONObject(); - JSONObject androidVRInnerTubeBody = new JSONObject(); - JSONObject iOSInnerTubeBody = new JSONObject(); - JSONObject tvEmbedInnerTubeBody = new JSONObject(); - JSONObject webInnerTubeBody = new JSONObject(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID"); - client.put("clientVersion", ANDROID_CLIENT_VERSION); - client.put("platform", "MOBILE"); - client.put("androidSdkVersion", ANDROID_OS_SDK_VERSION); - client.put("osName", "Android"); - client.put("osVersion", ANDROID_OS_RELEASE_VERSION); - - context.put("client", client); - - androidInnerTubeBody.put("context", context); - androidInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create Android innerTubeBody", e); - } - - ANDROID_INNER_TUBE_BODY = androidInnerTubeBody.toString(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID_EMBEDDED_PLAYER"); - client.put("clientVersion", ANDROID_CLIENT_VERSION); - client.put("clientScreen", "EMBED"); - client.put("platform", "MOBILE"); - client.put("androidSdkVersion", ANDROID_OS_SDK_VERSION); - client.put("osName", "Android"); - client.put("osVersion", ANDROID_OS_RELEASE_VERSION); - - JSONObject thirdParty = new JSONObject(); - thirdParty.put("embedUrl", "https://www.youtube.com/embed/%s"); - - context.put("thirdParty", thirdParty); - context.put("client", client); - - androidEmbedInnerTubeBody.put("context", context); - androidEmbedInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create Android Embed innerTubeBody", e); - } - - ANDROID_EMBED_INNER_TUBE_BODY = androidEmbedInnerTubeBody.toString(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID_TESTSUITE"); - client.put("clientVersion", ANDROID_TESTSUITE_CLIENT_VERSION); - client.put("platform", "MOBILE"); - client.put("androidSdkVersion", ANDROID_OS_SDK_VERSION); - client.put("osName", "Android"); - client.put("osVersion", ANDROID_OS_RELEASE_VERSION); - - context.put("client", client); - - androidTestsuiteInnerTubeBody.put("context", context); - androidTestsuiteInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create Android Testsuite innerTubeBody", e); - } - - ANDROID_TESTSUITE_INNER_TUBE_BODY = androidTestsuiteInnerTubeBody.toString(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID_UNPLUGGED"); - client.put("clientVersion", ANDROID_UNPLUGGED_CLIENT_VERSION); - client.put("platform", "MOBILE"); - client.put("androidSdkVersion", ANDROID_UNPLUGGED_OS_SDK_VERSION); - client.put("osName", "Android"); - client.put("osVersion", ANDROID_UNPLUGGED_OS_RELEASE_VERSION); - - context.put("client", client); - - androidUnpluggedInnerTubeBody.put("context", context); - androidUnpluggedInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create Android Unplugged innerTubeBody", e); - } - - ANDROID_UNPLUGGED_INNER_TUBE_BODY = androidUnpluggedInnerTubeBody.toString(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID_VR"); - client.put("clientVersion", ANDROID_VR_CLIENT_VERSION); - client.put("platform", "MOBILE"); - client.put("androidSdkVersion", ANDROID_VR_OS_SDK_VERSION); - client.put("osName", "Android"); - client.put("osVersion", ANDROID_VR_OS_RELEASE_VERSION); - - context.put("client", client); - - androidVRInnerTubeBody.put("context", context); - androidVRInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create Android VR innerTubeBody", e); - } - - ANDROID_VR_INNER_TUBE_BODY = androidVRInnerTubeBody.toString(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "IOS"); - client.put("clientVersion", IOS_CLIENT_VERSION); - client.put("deviceMake", "Apple"); - client.put("deviceModel", IOS_DEVICE_MODEL); - client.put("platform", "MOBILE"); - client.put("osName", "iOS"); - client.put("osVersion", IOS_OS_VERSION); - - context.put("client", client); - - iOSInnerTubeBody.put("context", context); - iOSInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create iOS innerTubeBody", e); - } - - IOS_INNER_TUBE_BODY = iOSInnerTubeBody.toString(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER"); - client.put("clientVersion", TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION); - client.put("platform", "TV"); - client.put("clientScreen", "EMBED"); - - JSONObject thirdParty = new JSONObject(); - thirdParty.put("embedUrl", "https://www.youtube.com/watch?v=%s"); - - context.put("thirdParty", thirdParty); - context.put("client", client); - - tvEmbedInnerTubeBody.put("context", context); - tvEmbedInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create TV Embed innerTubeBody", e); - } - - TVHTML5_SIMPLY_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString(); - - try { - JSONObject context = new JSONObject(); - - JSONObject client = new JSONObject(); - client.put("clientName", "WEB"); - client.put("clientVersion", WEB_CLIENT_VERSION); - client.put("clientScreen", "WATCH"); - - context.put("client", client); - - webInnerTubeBody.put("context", context); - webInnerTubeBody.put("videoId", "%s"); - } catch (JSONException e) { - Logger.printException(() -> "Failed to create Web innerTubeBody", e); - } - - WEB_INNER_TUBE_BODY = webInnerTubeBody.toString(); - } - - // Must check for device features in a separate class and cannot place this code inside - // the Patch or ClientType enum due to cyclic Setting references. - static class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); - - private static boolean deviceHasVP9HardwareDecoding() { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - ? codecInfo.isHardwareAccelerated() - : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. - if (isHardwareAccelerated && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - Logger.printDebug(() -> "Device supports VP9 hardware decoding."); - return true; - } - } - } - } - - Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); - return false; - } - - private static boolean deviceHasAV1HardwareDecoding() { - // It appears all devices with hardware AV1 are also Android 10 or newer. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/av01")) { - Logger.printDebug(() -> "Device supports AV1 hardware decoding."); - return true; - } - } - } - } - } - - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); - return false; - } - - static boolean allowVP9() { - return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get(); - } - - static boolean allowAV1() { - return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; - } - } - - private PlayerRoutes() { - } - - /** - * @noinspection SameParameterValue - */ - public static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, String userAgent) throws IOException { - var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); - - connection.setRequestProperty("User-Agent", userAgent); - connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); - connection.setRequestProperty("Content-Type", "application/json"); - - connection.setUseCaches(false); - connection.setDoOutput(true); - - connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); - connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); - return connection; - } - - public static final class SpoofingToIOSAvailability implements Setting.Availability { - public static boolean clientTypeIOSEnabled() { - final ClientType clientTypeIOS = ClientType.IOS; - return Settings.SPOOF_CLIENT_GENERAL.get() == clientTypeIOS || - Settings.SPOOF_CLIENT_LIVESTREAM.get() == clientTypeIOS || - Settings.SPOOF_CLIENT_SHORTS.get() == clientTypeIOS || - Settings.SPOOF_CLIENT_FALLBACK.get() == clientTypeIOS; - } - - @Override - public boolean isAvailable() { - return clientTypeIOSEnabled(); - } - } - - public enum ClientType { - ANDROID(3, ANDROID_DEVICE_MODEL, ANDROID_CLIENT_VERSION, ANDROID_INNER_TUBE_BODY, ANDROID_OS_RELEASE_VERSION, ANDROID_USER_AGENT), - ANDROID_EMBEDDED_PLAYER(55, ANDROID_DEVICE_MODEL, ANDROID_CLIENT_VERSION, ANDROID_EMBED_INNER_TUBE_BODY, ANDROID_OS_RELEASE_VERSION, ANDROID_USER_AGENT), - ANDROID_TESTSUITE(30, ANDROID_DEVICE_MODEL, ANDROID_TESTSUITE_CLIENT_VERSION, ANDROID_TESTSUITE_INNER_TUBE_BODY, ANDROID_OS_RELEASE_VERSION, ANDROID_USER_AGENT), - ANDROID_UNPLUGGED(29, ANDROID_UNPLUGGED_DEVICE_MODEL, ANDROID_UNPLUGGED_CLIENT_VERSION, ANDROID_UNPLUGGED_INNER_TUBE_BODY, ANDROID_UNPLUGGED_OS_RELEASE_VERSION, ANDROID_UNPLUGGED_USER_AGENT), - ANDROID_VR(28, ANDROID_VR_DEVICE_MODEL, ANDROID_VR_CLIENT_VERSION, ANDROID_VR_INNER_TUBE_BODY, ANDROID_VR_OS_RELEASE_VERSION, ANDROID_VR_USER_AGENT), - IOS(5, IOS_DEVICE_MODEL, IOS_CLIENT_VERSION, IOS_INNER_TUBE_BODY, IOS_OS_VERSION, IOS_USER_AGENT), - // No suitable model name was found for TVHTML5_SIMPLY_EMBEDDED_PLAYER. Use the model name of ANDROID. - TVHTML5_SIMPLY_EMBEDDED_PLAYER(85, ANDROID_DEVICE_MODEL, TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION, TVHTML5_SIMPLY_EMBED_INNER_TUBE_BODY, TVHTML5_SIMPLY_EMBEDDED_PLAYER_CLIENT_VERSION, TVHTML5_SIMPLY_EMBEDDED_PLAYER_USER_AGENT), - // No suitable model name was found for WEB. Use the model name of ANDROID. - WEB(1, ANDROID_DEVICE_MODEL, WEB_CLIENT_VERSION, WEB_INNER_TUBE_BODY, WEB_OS_VERSION, WEB_USER_AGENT); - - public final String friendlyName; - public final int id; - public final String model; - public final String appVersion; - public final String innerTubeBody; - public final String osVersion; - public final String userAgent; - - ClientType(int id, String model, String appVersion, String innerTubeBody, - String osVersion, String userAgent) { - this.friendlyName = str("revanced_spoof_client_options_entry_" + name().toLowerCase()); - this.id = id; - this.model = model; - this.appVersion = appVersion; - this.innerTubeBody = innerTubeBody; - this.osVersion = osVersion; - this.userAgent = userAgent; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StoryboardRendererRequester.java deleted file mode 100644 index 75b1b28aa6..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StoryboardRendererRequester.java +++ /dev/null @@ -1,184 +0,0 @@ -package app.revanced.integrations.youtube.patches.misc.requests; - -import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_STORYBOARD_SPEC_RENDERER; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; - -import app.revanced.integrations.shared.requests.Requester; -import app.revanced.integrations.shared.utils.Logger; -import app.revanced.integrations.shared.utils.Utils; -import app.revanced.integrations.youtube.patches.misc.StoryboardRenderer; -import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.ClientType; - -public class StoryboardRendererRequester { - - private StoryboardRendererRequester() { - } - - private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static JSONObject fetchPlayerResponse(@NonNull String requestBody, - @NonNull String userAgent) { - final long startTime = System.currentTimeMillis(); - try { - Utils.verifyOffMainThread(); - Objects.requireNonNull(requestBody); - - final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); - - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER, userAgent); - connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length); - - final int responseCode = connection.getResponseCode(); - if (responseCode == 200) return Requester.parseJSONObject(connection); - - // Always show a toast for this, as a non 200 response means something is broken. - handleConnectionError("Spoof storyboard not available: " + responseCode, null); - connection.disconnect(); - } catch (SocketTimeoutException ex) { - handleConnectionError("Spoof storyboard temporarily not available (API timed out)", ex); - } catch (IOException ex) { - handleConnectionError("Spoof storyboard temporarily not available: " + ex.getMessage(), ex); - } catch (Exception ex) { - Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen. - } finally { - Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - private static String getPlayabilityStatus(@NonNull JSONObject playerResponse) { - try { - return playerResponse.getJSONObject("playabilityStatus").getString("status"); - } catch (JSONException e) { - Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); - } - - // Prevent NullPointerException - return ""; - } - - /** - * Fetches the storyboardRenderer from the innerTubeBody. - * - * @param innerTubeBody The innerTubeBody to use to fetch the storyboardRenderer. - * @return StoryboardRenderer or null if playabilityStatus is not OK. - */ - @Nullable - private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String videoId, - @NonNull String innerTubeBody, - @NonNull String userAgent) { - final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, userAgent); - if (playerResponse == null) - return null; - - final String playabilityStatus = getPlayabilityStatus(playerResponse); - if (playabilityStatus.equals("OK")) - return getStoryboardRendererUsingResponse(videoId, playerResponse); - - // Get the StoryboardRenderer from Premieres Video. - // In Android client, YouTube used weird base64-like encoding for PlayerResponse. - // So additional fetching with WEB client is required for getting unSerialized ones. - if (playabilityStatus.equals("LIVE_STREAM_OFFLINE")) - return getTrailerStoryboardRenderer(videoId); - return null; - } - - @Nullable - private static StoryboardRenderer getTrailerStoryboardRenderer(@NonNull String videoId) { - try { - final ClientType requestClient = ClientType.WEB; - final JSONObject playerResponse = fetchPlayerResponse(String.format(requestClient.innerTubeBody, videoId), requestClient.userAgent); - - if (playerResponse == null) - return null; - - JSONObject unSerializedPlayerResponse = playerResponse.getJSONObject("playabilityStatus") - .getJSONObject("errorScreen").getJSONObject("ypcTrailerRenderer").getJSONObject("unserializedPlayerResponse"); - - if (getPlayabilityStatus(unSerializedPlayerResponse).equals("OK")) - return getStoryboardRendererUsingResponse(videoId, unSerializedPlayerResponse); - return null; - } catch (JSONException e) { - Logger.printException(() -> "Failed to get unserializedPlayerResponse", e); - } - - return null; - } - - @Nullable - private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull String videoId, @NonNull JSONObject playerResponse) { - try { - Logger.printDebug(() -> "Parsing storyboardRenderer from response: " + playerResponse); - if (!playerResponse.has("storyboards")) { - Logger.printDebug(() -> "Using empty storyboard"); - return new StoryboardRenderer(videoId, null, false, null); - } - final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); - final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer"); - final String storyboardsRendererTag = isLiveStream - ? "playerLiveStoryboardSpecRenderer" - : "playerStoryboardSpecRenderer"; - - final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); - StoryboardRenderer renderer = new StoryboardRenderer( - videoId, - rendererElement.getString("spec"), - isLiveStream, - rendererElement.has("recommendedLevel") - ? rendererElement.getInt("recommendedLevel") - : null - ); - - Logger.printDebug(() -> "Fetched: " + renderer); - - return renderer; - } catch (JSONException e) { - Logger.printException(() -> "Failed to get storyboardRenderer", e); - } - - return null; - } - - @Nullable - public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) { - Objects.requireNonNull(videoId); - - // Fetch with iOS. - ClientType clientType = ClientType.IOS; - StoryboardRenderer renderer = getStoryboardRendererUsingBody( - videoId, - String.format(clientType.innerTubeBody, videoId), - clientType.userAgent - ); - if (renderer == null) { - Logger.printDebug(() -> videoId + " not available using iOS client"); - - clientType = ClientType.TVHTML5_SIMPLY_EMBEDDED_PLAYER; - renderer = getStoryboardRendererUsingBody( - videoId, - String.format(clientType.innerTubeBody, videoId, videoId), - clientType.userAgent - ); - if (renderer == null) { - Logger.printDebug(() -> videoId + " not available using TV html5 embedded client"); - } - } - - return renderer; - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java index 9d951380a1..5e73a29db1 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java @@ -27,11 +27,6 @@ public static boolean SponsorBlock() { return false; } - public static boolean SpoofClient() { - // Replace this with true if the Spoof client patch succeeds - return false; - } - public static boolean ToolBarComponents() { // Replace this with true if the Toolbar components patch succeeds return false; diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 45bb7f9d4d..4c1bc0f898 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -32,8 +32,6 @@ import app.revanced.integrations.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption; import app.revanced.integrations.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime; import app.revanced.integrations.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType; -import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes; -import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.ClientType; import app.revanced.integrations.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; @@ -456,20 +454,6 @@ public class Settings extends BaseSettings { public static final BooleanSetting ENABLE_EXTERNAL_BROWSER = new BooleanSetting("revanced_enable_external_browser", TRUE, true); public static final BooleanSetting ENABLE_OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_enable_open_links_directly", TRUE); public static final BooleanSetting DISABLE_QUIC_PROTOCOL = new BooleanSetting("revanced_disable_quic_protocol", FALSE, true); - public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true, "revanced_spoof_client_user_dialog_message"); - public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true, "revanced_spoof_client_ios_force_avc_user_dialog_message", new PlayerRoutes.SpoofingToIOSAvailability()); - public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT)); - public static final BooleanSetting SPOOF_CLIENT_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_client_stats_for_nerds", TRUE, parent(SPOOF_CLIENT)); - public static final EnumSetting SPOOF_CLIENT_GENERAL = new EnumSetting<>("revanced_spoof_client_general", - ClientType.IOS); - public static final EnumSetting SPOOF_CLIENT_LIVESTREAM = new EnumSetting<>("revanced_spoof_client_livestream", - ClientType.IOS); - public static final EnumSetting SPOOF_CLIENT_SHORTS = new EnumSetting<>("revanced_spoof_client_shorts", - ClientType.IOS); - public static final EnumSetting SPOOF_CLIENT_FALLBACK = new EnumSetting<>("revanced_spoof_client_fallback", - // Some private videos cannot be played with {@code ClientType.IOS}. - // Use {@code ClientType.ANDROID_TESTSUITE}. - ClientType.ANDROID_TESTSUITE); public static final EnumSetting WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE); diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java index bea2adf2ba..f789f2d73d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WatchHistoryStatusPreference.java @@ -1,7 +1,6 @@ package app.revanced.integrations.youtube.settings.preference; import static app.revanced.integrations.shared.utils.StringRef.str; -import static app.revanced.integrations.youtube.patches.utils.PatchStatus.SpoofClient; import android.content.Context; import android.content.SharedPreferences; @@ -12,18 +11,17 @@ import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.shared.utils.Utils; import app.revanced.integrations.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType; -import app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.SpoofingToIOSAvailability; import app.revanced.integrations.youtube.settings.Settings; -@SuppressWarnings("unused") +@SuppressWarnings({"deprecation", "unused"}) public class WatchHistoryStatusPreference extends Preference { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { - // Because this listener may run before the ReVanced settings fragment updates SettingsEnum, + // Because this listener may run before the ReVanced settings fragment updates Settings, // this could show the prior config and not the current. // // Push this call to the end of the main run queue, - // so all other listeners are done and SettingsEnum is up to date. + // so all other listeners are done and Settings is up to date. Utils.runOnMainThread(this::updateUI); }; @@ -65,9 +63,6 @@ protected void onPrepareForRemoval() { } private void updateUI() { - final boolean spoofClientEnabled = SpoofClient() && Settings.SPOOF_CLIENT.get(); - final boolean containsClientTypeIOS = SpoofingToIOSAvailability.clientTypeIOSEnabled(); - final WatchHistoryType watchHistoryType = Settings.WATCH_HISTORY_TYPE.get(); final boolean blockWatchHistory = watchHistoryType == WatchHistoryType.BLOCK; final boolean replaceWatchHistory = watchHistoryType == WatchHistoryType.REPLACE; @@ -75,14 +70,10 @@ private void updateUI() { final String summaryTextKey; if (blockWatchHistory) { summaryTextKey = "revanced_watch_history_about_status_blocked"; - } else if (spoofClientEnabled && containsClientTypeIOS) { - summaryTextKey = replaceWatchHistory - ? "revanced_watch_history_about_status_ios_replaced" - : "revanced_watch_history_about_status_ios_original"; + } else if (replaceWatchHistory) { + summaryTextKey = "revanced_watch_history_about_status_replaced"; } else { - summaryTextKey = replaceWatchHistory - ? "revanced_watch_history_about_status_android_replaced" - : "revanced_watch_history_about_status_android_original"; + summaryTextKey = "revanced_watch_history_about_status_original"; } setSummary(str(summaryTextKey));