From c342d6f852e02ee7e2f1152fc71792fb8b4e4395 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Thu, 3 Oct 2024 21:52:17 +0900 Subject: [PATCH] fix(YouTube - Spoof streaming data): Playback issues can occur when the data connection changes or when RVX has been open for a long time fix(YouTube - Spoof streaming data): Wrong package name used in User-Agent fix(YouTube - Spoof streaming data): `Authorization` key is always included when fetching an API, even if there is no `Authorization` in the header (e.g. the user is not logged in or using the Incognito Mode) fix(YouTube - Spoof streaming data): Revert `reduce response timeout and cache size` --- .../patches/misc/SpoofStreamingDataPatch.java | 19 +++---- .../patches/misc/client/AppClient.java | 2 +- .../misc/requests/StreamingDataRequest.java | 51 ++++++------------- 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java index 59ddc6da57..a00bac4521 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofStreamingDataPatch.java @@ -19,7 +19,6 @@ @SuppressWarnings("unused") public class SpoofStreamingDataPatch { private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_STREAMING_DATA.get(); - private static final ClientType SPOOF_STREAMING_DATA_TYPE = Settings.SPOOF_STREAMING_DATA_TYPE.get(); /** * Any unreachable ip address. Used to intentionally fail requests. @@ -56,6 +55,10 @@ public static Uri blockGetWatchRequest(Uri playerRequestUri) { * Injection point. *

* Blocks /initplayback requests. + *

+ * In some cases, blocking all URLs containing the path `initplayback` + * using localhost can also cause playback issues. + * See this GitHub Issue. */ public static String blockInitPlaybackRequest(String originalUrlString) { if (SPOOF_STREAMING_DATA) { @@ -64,17 +67,9 @@ public static String blockInitPlaybackRequest(String originalUrlString) { String path = originalUri.getPath(); if (path != null && path.contains("initplayback")) { - String replacementUriString = (SPOOF_STREAMING_DATA_TYPE == ClientType.IOS) - ? 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 'initplayback' by returning unreachable url"); + Logger.printDebug(() -> "Blocking 'initplayback' by clearing query"); - return replacementUriString; + return originalUri.buildUpon().clearQuery().build().toString(); } } catch (Exception ex) { Logger.printException(() -> "blockInitPlaybackRequest failure", ex); @@ -158,7 +153,7 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos String path = uri.getPath(); String clientNameQueryKey = "c"; final boolean iosClient = "IOS".equals(uri.getQueryParameter(clientNameQueryKey)); - if (iosClient && path != null && path.contains("videoplayback")) { + if (path != null && path.contains("videoplayback")) { return null; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java index 0a57ef86be..21fc215be9 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java @@ -112,7 +112,7 @@ public class AppClient { * 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 USER_AGENT_ANDROID_VR = "com.google.android.youtube/" + + private static final String USER_AGENT_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus/" + CLIENT_VERSION_ANDROID_VR + " (Linux; U; Android " + OS_VERSION_ANDROID_VR + diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java index 9ce54a41e2..0ae86b1aa1 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java @@ -59,11 +59,6 @@ public static String getLastSpoofedClientName() { : lastSpoofedClientType.friendlyName; } - /** - * How long to keep fetches until they are expired. - */ - private static final long CACHE_RETENTION_TIME_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes - /** * TCP connection and HTTP read timeout. */ @@ -76,7 +71,7 @@ public static String getLastSpoofedClientName() { @GuardedBy("itself") private static final Map cache = Collections.synchronizedMap( - new LinkedHashMap<>(30) { + new LinkedHashMap<>(100) { /** * Cache limit must be greater than the maximum number of videos open at once, * which theoretically is more than 4 (3 Shorts + one regular minimized video). @@ -84,7 +79,7 @@ public static String getLastSpoofedClientName() { * is somehow still referenced. Each stream is a small array of Strings * so memory usage is not a concern. */ - private static final int CACHE_LIMIT = 15; + private static final int CACHE_LIMIT = 50; @Override protected boolean removeEldestEntry(Entry eldest) { @@ -93,29 +88,24 @@ protected boolean removeEldestEntry(Entry eldest) { }); public static void fetchRequest(@NonNull String videoId, Map fetchHeaders) { - synchronized (cache) { - // Remove any expired entries. - final long now = System.currentTimeMillis(); - cache.values().removeIf(request -> { - final boolean expired = request.isExpired(now); - if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId); - return expired; - }); - cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); - } + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); } @Nullable public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) { - synchronized (cache) { - return cache.get(videoId); - } + return cache.get(videoId); } private static void handleConnectionError(String toastMessage, @Nullable Exception ex) { Logger.printInfo(() -> toastMessage, ex); } + private static final String[] REQUEST_HEADER_KEYS = { + "Authorization", // Available only to logged in users. + "X-GOOG-API-FORMAT-VERSION", + "X-Goog-Visitor-Id" + }; + @Nullable private static HttpURLConnection send(ClientType clientType, String videoId, Map playerHeaders) { @@ -132,10 +122,11 @@ private static HttpURLConnection send(ClientType clientType, String videoId, connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); - String authHeader = playerHeaders.get("Authorization"); - String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); - connection.setRequestProperty("Authorization", authHeader); - connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); + for (String key : REQUEST_HEADER_KEYS) { + if (playerHeaders.containsKey(key)) { + connection.setRequestProperty(key, playerHeaders.get(key)); + } + } String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId); byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); @@ -195,27 +186,15 @@ private static ByteBuffer fetch(@NonNull String videoId, Map pla return null; } - private final long timeFetched; private final String videoId; private final Future future; private StreamingDataRequest(String videoId, Map playerHeaders) { Objects.requireNonNull(playerHeaders); - this.timeFetched = System.currentTimeMillis(); this.videoId = videoId; this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); } - public boolean isExpired(long now) { - final long timeSinceCreation = now - timeFetched; - if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) { - return true; - } - - // Only expired if the fetch failed (API null response). - return (fetchCompleted() && getStream() == null); - } - public boolean fetchCompleted() { return future.isDone(); }