From cc2ac4e4cd15ca2a23d60abd160d915bc98f99b4 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 27 Nov 2024 14:31:52 +0100 Subject: [PATCH] fix(YouTube - Spoof video streams): Log out the iOS client to restore kids videos playback (#4000) --- .../youtube/patches/spoof/ClientType.java | 52 +++++++++++---- .../patches/spoof/requests/PlayerRoutes.java | 14 ++--- .../spoof/requests/StreamingDataRequest.java | 63 ++++++++++--------- .../resources/addresources/values/arrays.xml | 2 +- .../resources/addresources/values/strings.xml | 4 +- 5 files changed, 81 insertions(+), 54 deletions(-) diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/ClientType.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/ClientType.java index de6a2a12c..2c1f0d55f 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/ClientType.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/ClientType.java @@ -8,6 +8,17 @@ import android.os.Build; import androidx.annotation.Nullable; public enum ClientType { + // Specific purpose for age restricted, or private videos, because the iOS client is not logged in. + ANDROID_VR(28, + "Quest 3", + "12", + "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", + "32", // Android 12.1 + "1.56.21", + "ANDROID_VR", + true + ), + // Specific for kids videos. // https://dumps.tadiphone.dev/dumps/oculus/eureka IOS(5, // iPhone 15 supports AV1 hardware decoding. @@ -25,14 +36,9 @@ public enum ClientType { null, // Version number should be a valid iOS release. // https://www.ipa4fun.com/history/185230 - "19.10.7" - ), - ANDROID_VR(28, - "Quest 3", - "12", - "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "32", // Android 12.1 - "1.56.21" + "19.10.7", + "IOS", + false ); /** @@ -44,7 +50,7 @@ public enum ClientType { /** * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) */ - public final String model; + public final String deviceModel; /** * Device OS version. @@ -63,17 +69,37 @@ public enum ClientType { @Nullable public final String androidSdkVersion; + /** + * Client name. + */ + public final String clientName; + /** * App version. */ - public final String appVersion; + public final String clientVersion; - ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) { + /** + * If the client can access the API logged in. + */ + public final boolean canLogin; + + ClientType(int id, + String deviceModel, + String osVersion, + String userAgent, + @Nullable String androidSdkVersion, + String clientVersion, + String clientName, + boolean canLogin + ) { this.id = id; - this.model = model; + this.deviceModel = deviceModel; this.osVersion = osVersion; this.userAgent = userAgent; this.androidSdkVersion = androidSdkVersion; - this.appVersion = appVersion; + this.clientVersion = clientVersion; + this.clientName = clientName; + this.canLogin = canLogin; } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/PlayerRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/PlayerRoutes.java index 364dc173a..3388892cc 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/PlayerRoutes.java @@ -12,15 +12,13 @@ import app.revanced.extension.youtube.requests.Requester; import app.revanced.extension.youtube.requests.Route; final class PlayerRoutes { - private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; - static final Route.CompiledRoute GET_STREAMING_DATA = new Route( Route.Method.POST, "player" + "?fields=streamingData" + "&alt=proto" ).compile(); - + private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; /** * TCP connection and HTTP read timeout */ @@ -30,15 +28,15 @@ final class PlayerRoutes { } static String createInnertubeBody(ClientType clientType) { - JSONObject innerTubeBody = new JSONObject(); + JSONObject innerTubeBody = new JSONObject(); try { JSONObject context = new JSONObject(); JSONObject client = new JSONObject(); client.put("clientName", clientType.name()); - client.put("clientVersion", clientType.appVersion); - client.put("deviceModel", clientType.model); + client.put("clientVersion", clientType.clientVersion); + client.put("deviceModel", clientType.deviceModel); client.put("osVersion", clientType.osVersion); if (clientType.androidSdkVersion != null) { client.put("androidSdkVersion", clientType.androidSdkVersion); @@ -57,7 +55,9 @@ final class PlayerRoutes { return innerTubeBody.toString(); } - /** @noinspection SameParameterValue*/ + /** + * @noinspection SameParameterValue + */ static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/StreamingDataRequest.java index e66f4d885..0c09e0fa7 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -28,7 +28,7 @@ import app.revanced.extension.youtube.settings.Settings; /** * Video streaming data. Fetching is tied to the behavior YT uses, * where this class fetches the streams only when YT fetches. - * + *

* Effectively the cache expiration of these fetches is the same as the stock app, * since the stock app would not use expired streams and therefor * the extension replace stream hook is called only if YT @@ -37,38 +37,20 @@ import app.revanced.extension.youtube.settings.Settings; public class StreamingDataRequest { private static final ClientType[] CLIENT_ORDER_TO_USE; - - static { - ClientType[] allClientTypes = ClientType.values(); - ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); - - CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length]; - CLIENT_ORDER_TO_USE[0] = preferredClient; - - int i = 1; - for (ClientType c : allClientTypes) { - if (c != preferredClient) { - CLIENT_ORDER_TO_USE[i++] = c; - } - } - } - + private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String[] REQUEST_HEADER_KEYS = { - "Authorization", // Available only to logged in users. + AUTHORIZATION_HEADER, // Available only to logged-in users. "X-GOOG-API-FORMAT-VERSION", "X-Goog-Visitor-Id" }; - /** * TCP connection and HTTP read timeout. */ private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000; - /** * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS} */ private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; - private static final Map cache = Collections.synchronizedMap( new LinkedHashMap<>(100) { /** @@ -86,8 +68,32 @@ public class StreamingDataRequest { } }); + static { + ClientType[] allClientTypes = ClientType.values(); + ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); + + CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length]; + CLIENT_ORDER_TO_USE[0] = preferredClient; + + int i = 1; + for (ClientType c : allClientTypes) { + if (c != preferredClient) { + CLIENT_ORDER_TO_USE[i++] = c; + } + } + } + + private final String videoId; + private final Future future; + + private StreamingDataRequest(String videoId, Map playerHeaders) { + Objects.requireNonNull(playerHeaders); + this.videoId = videoId; + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); + } + public static void fetchRequest(String videoId, Map fetchHeaders) { - // Always fetch, even if there is a existing request for the same video. + // Always fetch, even if there is an existing request for the same video. cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); } @@ -119,6 +125,10 @@ public class StreamingDataRequest { connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); for (String key : REQUEST_HEADER_KEYS) { + if (!clientType.canLogin && key.equals(AUTHORIZATION_HEADER)) { + continue; + } + String value = playerHeaders.get(key); if (value != null) { connection.setRequestProperty(key, value); @@ -186,15 +196,6 @@ public class StreamingDataRequest { return null; } - private final String videoId; - private final Future future; - - private StreamingDataRequest(String videoId, Map playerHeaders) { - Objects.requireNonNull(playerHeaders); - this.videoId = videoId; - this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); - } - public boolean fetchCompleted() { return future.isDone(); } diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 7dbc76b2c..508e9381f 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -176,7 +176,7 @@ - + @string/revanced_block_embedded_ads_entry_1 @string/revanced_block_embedded_ads_entry_2 diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index c8bee767d..0b1a2df63 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1225,9 +1225,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Your device does not have VP9 hardware decoding, and this setting is always on when Client spoofing is enabled Enabling this might improve battery life and fix playback stuttering.\n\nAVC has a maximum resolution of 1080p, and video playback will use more internet data than VP9 or AV1. iOS spoofing side effects - • Movies or paid videos may not play\n• Livestreams start from the beginning\n• Videos may end 1 second early\n• No opus audio codec + • Private kids videos may not play\n• Livestreams start from the beginning\n• Videos may end 1 second early\n• No opus audio codec Android VR spoofing side effects - • Audio track menu is missing\n• Stable volume is not available + • Kids videos may not play\n• Audio track menu is missing\n• Stable volume is not available