From 7917871f510b6b805370ef98a0cf8a4e2df0e900 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:49:00 +0200 Subject: [PATCH] fix(YouTube - Spoof video streams): Update client user-agent (#4304) --- .../extension/shared/spoof/ClientType.java | 143 ++++++++++++++---- .../shared/spoof/requests/PlayerRoutes.java | 23 +-- .../spoof/requests/StreamingDataRequest.java | 20 ++- 3 files changed, 141 insertions(+), 45 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java index bfa5af526..daf555068 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java @@ -4,6 +4,10 @@ import android.os.Build; import androidx.annotation.Nullable; +import java.util.Locale; +import java.util.Objects; + +import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.settings.BaseSettings; public enum ClientType { @@ -11,13 +15,17 @@ public enum ClientType { ANDROID_VR_NO_AUTH( 28, "ANDROID_VR", + "com.google.android.apps.youtube.vr.oculus", "Oculus", "Quest 3", "Android", "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 12.1 + "32", + "SQ3A.220605.009.A1", + "132.0.6808.3", + "1.61.48", + false, false, "Android VR No auth" ), @@ -26,67 +34,81 @@ public enum ClientType { ANDROID_UNPLUGGED( 29, "ANDROID_UNPLUGGED", + "com.google.android.apps.youtube.unplugged", "Google", "Google TV Streamer", "Android", "14", - "com.google.android.apps.youtube.unplugged/8.49.0 (Linux; U; Android 14; GB) gzip", "34", + "UTT3.240625.001.K5", + "132.0.6808.3", "8.49.0", true, + true, "Android TV" ), // Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children". + // Google Pixel 9 Pro Fold + // https://dumps.tadiphone.dev/dumps/google/barbet ANDROID_CREATOR( 14, "ANDROID_CREATOR", - Build.MANUFACTURER, - Build.MODEL, + "com.google.android.apps.youtube.creator", + "Google", + "Pixel 9 Pro Fold", "Android", - "11", - "com.google.android.apps.youtube.creator/24.45.100 (Linux; U; Android 11) gzip", - "30", - "24.45.100", + "15", + "35", + "AP3A.241005.015.A2", + "132.0.6779.0", + "23.47.101", + true, true, "Android Creator" ), ANDROID_VR( ANDROID_VR_NO_AUTH.id, ANDROID_VR_NO_AUTH.clientName, + ANDROID_VR_NO_AUTH.packageName, ANDROID_VR_NO_AUTH.deviceMake, ANDROID_VR_NO_AUTH.deviceModel, ANDROID_VR_NO_AUTH.osName, ANDROID_VR_NO_AUTH.osVersion, - ANDROID_VR_NO_AUTH.userAgent, ANDROID_VR_NO_AUTH.androidSdkVersion, + ANDROID_VR_NO_AUTH.buildId, + ANDROID_VR_NO_AUTH.cronetVersion, ANDROID_VR_NO_AUTH.clientVersion, + ANDROID_VR_NO_AUTH.requiresAuth, true, "Android VR" ), IOS_UNPLUGGED( 33, "IOS_UNPLUGGED", + "com.google.ios.youtubeunplugged", "Apple", forceAVC() - ? "iPhone12,5" // 11 Pro Max (last device with iOS 13) - : "iPhone16,2", // 15 Pro Max + // 11 Pro Max (last device with iOS 13) + ? "iPhone12,5" + // 15 Pro Max + : "iPhone16,2", "iOS", - // iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1. forceAVC() - ? "13.7.17H35" // Last release of iOS 13. + // iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1. + ? "13.7.17H35" : "18.2.22C152", - forceAVC() - ? "com.google.ios.youtubeunplugged/6.45 (iPhone12,5; U; CPU iOS 13_7 like Mac OS X)" - : "com.google.ios.youtubeunplugged/8.49 (iPhone16,2; U; CPU iOS 18_2_22 like Mac OS X)", + null, + null, null, // Version number should be a valid iOS release. // https://www.ipa4fun.com/history/152043/ - // Some newer versions can also force AVC, - // but 6.45 is the last version that supports iOS 13. forceAVC() + // Some newer versions can also force AVC, + // but 6.45 is the last version that supports iOS 13. ? "6.45" : "8.49", true, + true, forceAVC() ? "iOS TV Force AVC" : "iOS TV" @@ -104,6 +126,16 @@ public enum ClientType { public final String clientName; + /** + * App package name. + */ + private final String packageName; + + /** + * Player user-agent. + */ + public final String userAgent; + /** * Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer) */ @@ -124,11 +156,6 @@ public enum ClientType { */ public final String osVersion; - /** - * Player user-agent. - */ - public final String userAgent; - /** * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) * Field is null if not applicable. @@ -136,43 +163,97 @@ public enum ClientType { @Nullable public final String androidSdkVersion; + /** + * Android build id, equivalent to {@link Build#ID}. + * Field is null if not applicable. + */ + @Nullable + private final String buildId; + + /** + * Cronet release version, as found in decompiled client apk. + * Field is null if not applicable. + */ + @Nullable + private final String cronetVersion; + /** * App version. */ public final String clientVersion; /** - * If the client can access the API logged in. + * If this client requires authentication and does not work + * if logged out or in incognito mode. */ - public final boolean canLogin; + public final boolean requiresAuth; + + /** + * If the client should use authentication if available. + */ + public final boolean useAuth; /** * Friendly name displayed in stats for nerds. */ public final String friendlyName; + @SuppressWarnings("ConstantLocale") ClientType(int id, String clientName, + String packageName, String deviceMake, String deviceModel, String osName, String osVersion, - String userAgent, @Nullable String androidSdkVersion, + @Nullable String buildId, + @Nullable String cronetVersion, String clientVersion, - boolean canLogin, + boolean requiresAuth, + boolean useAuth, String friendlyName) { this.id = id; this.clientName = clientName; + this.packageName = packageName; this.deviceMake = deviceMake; this.deviceModel = deviceModel; this.osName = osName; this.osVersion = osVersion; - this.userAgent = userAgent; this.androidSdkVersion = androidSdkVersion; + this.buildId = buildId; + this.cronetVersion = cronetVersion; this.clientVersion = clientVersion; - this.canLogin = canLogin; + this.requiresAuth = requiresAuth; + this.useAuth = useAuth; this.friendlyName = friendlyName; + + Locale defaultLocale = Locale.getDefault(); + if (androidSdkVersion == null) { + // Convert version from '18.2.22C152' into '18_2_22' + String userAgentOsVersion = osVersion + .replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1") + .replace(".", "_"); + // https://github.com/mitmproxy/mitmproxy/issues/4836 + this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)", + packageName, + clientVersion, + deviceModel, + userAgentOsVersion, + defaultLocale + ); + } else { + this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)", + packageName, + clientVersion, + osVersion, + defaultLocale, + deviceModel, + Objects.requireNonNull(buildId), + Objects.requireNonNull(cronetVersion) + ); + } + Logger.printDebug(() -> "userAgent: " + this.userAgent); } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java index bae0c07a3..f45e890d5 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java @@ -5,12 +5,12 @@ import org.json.JSONObject; import java.io.IOException; import java.net.HttpURLConnection; +import java.util.Locale; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Route; import app.revanced.extension.shared.settings.BaseSettings; -import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.spoof.ClientType; final class PlayerRoutes { @@ -31,7 +31,7 @@ final class PlayerRoutes { private PlayerRoutes() { } - static String createInnertubeBody(ClientType clientType) { + static String createInnertubeBody(ClientType clientType, String videoId) { JSONObject innerTubeBody = new JSONObject(); try { @@ -42,27 +42,28 @@ final class PlayerRoutes { // but if this is a fall over client it will set the language even though // the audio language is not selectable in the UI. ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); - AppLanguage language = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH - ? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get() - : AppLanguage.DEFAULT; + Locale streamLocale = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH + ? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale() + : Locale.getDefault(); JSONObject client = new JSONObject(); - client.put("hl", language.getLanguage()); - client.put("clientName", clientType.clientName); - client.put("clientVersion", clientType.clientVersion); client.put("deviceMake", clientType.deviceMake); client.put("deviceModel", clientType.deviceModel); + client.put("clientName", clientType.clientName); + client.put("clientVersion", clientType.clientVersion); client.put("osName", clientType.osName); client.put("osVersion", clientType.osVersion); if (clientType.androidSdkVersion != null) { client.put("androidSdkVersion", clientType.androidSdkVersion); } + client.put("hl", streamLocale.getLanguage()); + client.put("gl", streamLocale.getCountry()); context.put("client", client); innerTubeBody.put("context", context); innerTubeBody.put("contentCheckOk", true); innerTubeBody.put("racyCheckOk", true); - innerTubeBody.put("videoId", "%s"); + innerTubeBody.put("videoId", videoId); } catch (JSONException e) { Logger.printException(() -> "Failed to create innerTubeBody", e); } @@ -78,7 +79,9 @@ final class PlayerRoutes { connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("User-Agent", clientType.userAgent); - connection.setRequestProperty("X-YouTube-Client-Version", String.valueOf(clientType.id)); + // Not a typo. "Client-Name" uses the client type id. + connection.setRequestProperty("X-YouTube-Client-Name", String.valueOf(clientType.id)); + connection.setRequestProperty("X-YouTube-Client-Version", clientType.clientVersion); connection.setUseCaches(false); connection.setDoOutput(true); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java index 5daa4b6e8..339ed2185 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java @@ -120,7 +120,8 @@ public class StreamingDataRequest { } @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, + private static HttpURLConnection send(ClientType clientType, + String videoId, Map playerHeaders, boolean showErrorToasts) { Objects.requireNonNull(clientType); @@ -128,21 +129,24 @@ public class StreamingDataRequest { Objects.requireNonNull(playerHeaders); final long startTime = System.currentTimeMillis(); - Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType); try { HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); + boolean authHeadersIncludes = false; + for (String key : REQUEST_HEADER_KEYS) { String value = playerHeaders.get(key); + if (value != null) { if (key.equals(AUTHORIZATION_HEADER)) { - if (!clientType.canLogin) { + if (!clientType.useAuth) { Logger.printDebug(() -> "Not including request header: " + key); continue; } + authHeadersIncludes = true; } Logger.printDebug(() -> "Including request header: " + key); @@ -150,7 +154,15 @@ public class StreamingDataRequest { } } - String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + if (!authHeadersIncludes && clientType.requiresAuth) { + Logger.printDebug(() -> "Skipping client since user is not logged in: " + clientType + + " videoId: " + videoId); + return null; + } + + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType); + + String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId); byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(requestBody.length); connection.getOutputStream().write(requestBody);