fix(YouTube - Spoof video streams): Update client user-agent (#4304)

This commit is contained in:
LisoUseInAIKyrios 2025-01-20 12:49:00 +02:00 committed by GitHub
parent e89fd80ec9
commit 7917871f51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 141 additions and 45 deletions

View File

@ -4,6 +4,10 @@ import android.os.Build;
import androidx.annotation.Nullable; 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; import app.revanced.extension.shared.settings.BaseSettings;
public enum ClientType { public enum ClientType {
@ -11,13 +15,17 @@ public enum ClientType {
ANDROID_VR_NO_AUTH( ANDROID_VR_NO_AUTH(
28, 28,
"ANDROID_VR", "ANDROID_VR",
"com.google.android.apps.youtube.vr.oculus",
"Oculus", "Oculus",
"Quest 3", "Quest 3",
"Android", "Android",
"12", "12",
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", // Android 12.1
"32", // Android 12.1 "32",
"1.56.21", "SQ3A.220605.009.A1",
"132.0.6808.3",
"1.61.48",
false,
false, false,
"Android VR No auth" "Android VR No auth"
), ),
@ -26,67 +34,81 @@ public enum ClientType {
ANDROID_UNPLUGGED( ANDROID_UNPLUGGED(
29, 29,
"ANDROID_UNPLUGGED", "ANDROID_UNPLUGGED",
"com.google.android.apps.youtube.unplugged",
"Google", "Google",
"Google TV Streamer", "Google TV Streamer",
"Android", "Android",
"14", "14",
"com.google.android.apps.youtube.unplugged/8.49.0 (Linux; U; Android 14; GB) gzip",
"34", "34",
"UTT3.240625.001.K5",
"132.0.6808.3",
"8.49.0", "8.49.0",
true, true,
true,
"Android TV" "Android TV"
), ),
// Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children". // 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( ANDROID_CREATOR(
14, 14,
"ANDROID_CREATOR", "ANDROID_CREATOR",
Build.MANUFACTURER, "com.google.android.apps.youtube.creator",
Build.MODEL, "Google",
"Pixel 9 Pro Fold",
"Android", "Android",
"11", "15",
"com.google.android.apps.youtube.creator/24.45.100 (Linux; U; Android 11) gzip", "35",
"30", "AP3A.241005.015.A2",
"24.45.100", "132.0.6779.0",
"23.47.101",
true,
true, true,
"Android Creator" "Android Creator"
), ),
ANDROID_VR( ANDROID_VR(
ANDROID_VR_NO_AUTH.id, ANDROID_VR_NO_AUTH.id,
ANDROID_VR_NO_AUTH.clientName, ANDROID_VR_NO_AUTH.clientName,
ANDROID_VR_NO_AUTH.packageName,
ANDROID_VR_NO_AUTH.deviceMake, ANDROID_VR_NO_AUTH.deviceMake,
ANDROID_VR_NO_AUTH.deviceModel, ANDROID_VR_NO_AUTH.deviceModel,
ANDROID_VR_NO_AUTH.osName, ANDROID_VR_NO_AUTH.osName,
ANDROID_VR_NO_AUTH.osVersion, ANDROID_VR_NO_AUTH.osVersion,
ANDROID_VR_NO_AUTH.userAgent,
ANDROID_VR_NO_AUTH.androidSdkVersion, ANDROID_VR_NO_AUTH.androidSdkVersion,
ANDROID_VR_NO_AUTH.buildId,
ANDROID_VR_NO_AUTH.cronetVersion,
ANDROID_VR_NO_AUTH.clientVersion, ANDROID_VR_NO_AUTH.clientVersion,
ANDROID_VR_NO_AUTH.requiresAuth,
true, true,
"Android VR" "Android VR"
), ),
IOS_UNPLUGGED( IOS_UNPLUGGED(
33, 33,
"IOS_UNPLUGGED", "IOS_UNPLUGGED",
"com.google.ios.youtubeunplugged",
"Apple", "Apple",
forceAVC() forceAVC()
? "iPhone12,5" // 11 Pro Max (last device with iOS 13) // 11 Pro Max (last device with iOS 13)
: "iPhone16,2", // 15 Pro Max ? "iPhone12,5"
// 15 Pro Max
: "iPhone16,2",
"iOS", "iOS",
forceAVC()
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1. // iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
forceAVC() ? "13.7.17H35"
? "13.7.17H35" // Last release of iOS 13.
: "18.2.22C152", : "18.2.22C152",
forceAVC() null,
? "com.google.ios.youtubeunplugged/6.45 (iPhone12,5; U; CPU iOS 13_7 like Mac OS X)" null,
: "com.google.ios.youtubeunplugged/8.49 (iPhone16,2; U; CPU iOS 18_2_22 like Mac OS X)",
null, null,
// Version number should be a valid iOS release. // Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/152043/ // https://www.ipa4fun.com/history/152043/
forceAVC()
// Some newer versions can also force AVC, // Some newer versions can also force AVC,
// but 6.45 is the last version that supports iOS 13. // but 6.45 is the last version that supports iOS 13.
forceAVC()
? "6.45" ? "6.45"
: "8.49", : "8.49",
true, true,
true,
forceAVC() forceAVC()
? "iOS TV Force AVC" ? "iOS TV Force AVC"
: "iOS TV" : "iOS TV"
@ -104,6 +126,16 @@ public enum ClientType {
public final String clientName; 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) * Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
*/ */
@ -124,11 +156,6 @@ public enum ClientType {
*/ */
public final String osVersion; 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) * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
* Field is null if not applicable. * Field is null if not applicable.
@ -136,43 +163,97 @@ public enum ClientType {
@Nullable @Nullable
public final String androidSdkVersion; 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. * App version.
*/ */
public final String clientVersion; 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. * Friendly name displayed in stats for nerds.
*/ */
public final String friendlyName; public final String friendlyName;
@SuppressWarnings("ConstantLocale")
ClientType(int id, ClientType(int id,
String clientName, String clientName,
String packageName,
String deviceMake, String deviceMake,
String deviceModel, String deviceModel,
String osName, String osName,
String osVersion, String osVersion,
String userAgent,
@Nullable String androidSdkVersion, @Nullable String androidSdkVersion,
@Nullable String buildId,
@Nullable String cronetVersion,
String clientVersion, String clientVersion,
boolean canLogin, boolean requiresAuth,
boolean useAuth,
String friendlyName) { String friendlyName) {
this.id = id; this.id = id;
this.clientName = clientName; this.clientName = clientName;
this.packageName = packageName;
this.deviceMake = deviceMake; this.deviceMake = deviceMake;
this.deviceModel = deviceModel; this.deviceModel = deviceModel;
this.osName = osName; this.osName = osName;
this.osVersion = osVersion; this.osVersion = osVersion;
this.userAgent = userAgent;
this.androidSdkVersion = androidSdkVersion; this.androidSdkVersion = androidSdkVersion;
this.buildId = buildId;
this.cronetVersion = cronetVersion;
this.clientVersion = clientVersion; this.clientVersion = clientVersion;
this.canLogin = canLogin; this.requiresAuth = requiresAuth;
this.useAuth = useAuth;
this.friendlyName = friendlyName; 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);
} }
} }

View File

@ -5,12 +5,12 @@ import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.util.Locale;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route; import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.spoof.ClientType; import app.revanced.extension.shared.spoof.ClientType;
final class PlayerRoutes { final class PlayerRoutes {
@ -31,7 +31,7 @@ final class PlayerRoutes {
private PlayerRoutes() { private PlayerRoutes() {
} }
static String createInnertubeBody(ClientType clientType) { static String createInnertubeBody(ClientType clientType, String videoId) {
JSONObject innerTubeBody = new JSONObject(); JSONObject innerTubeBody = new JSONObject();
try { try {
@ -42,27 +42,28 @@ final class PlayerRoutes {
// but if this is a fall over client it will set the language even though // but if this is a fall over client it will set the language even though
// the audio language is not selectable in the UI. // the audio language is not selectable in the UI.
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
AppLanguage language = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH Locale streamLocale = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get() ? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale()
: AppLanguage.DEFAULT; : Locale.getDefault();
JSONObject client = new JSONObject(); 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("deviceMake", clientType.deviceMake);
client.put("deviceModel", clientType.deviceModel); client.put("deviceModel", clientType.deviceModel);
client.put("clientName", clientType.clientName);
client.put("clientVersion", clientType.clientVersion);
client.put("osName", clientType.osName); client.put("osName", clientType.osName);
client.put("osVersion", clientType.osVersion); client.put("osVersion", clientType.osVersion);
if (clientType.androidSdkVersion != null) { if (clientType.androidSdkVersion != null) {
client.put("androidSdkVersion", clientType.androidSdkVersion); client.put("androidSdkVersion", clientType.androidSdkVersion);
} }
client.put("hl", streamLocale.getLanguage());
client.put("gl", streamLocale.getCountry());
context.put("client", client); context.put("client", client);
innerTubeBody.put("context", context); innerTubeBody.put("context", context);
innerTubeBody.put("contentCheckOk", true); innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true); innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", "%s"); innerTubeBody.put("videoId", videoId);
} catch (JSONException e) { } catch (JSONException e) {
Logger.printException(() -> "Failed to create innerTubeBody", e); Logger.printException(() -> "Failed to create innerTubeBody", e);
} }
@ -78,7 +79,9 @@ final class PlayerRoutes {
connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", clientType.userAgent); 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.setUseCaches(false);
connection.setDoOutput(true); connection.setDoOutput(true);

View File

@ -120,7 +120,8 @@ public class StreamingDataRequest {
} }
@Nullable @Nullable
private static HttpURLConnection send(ClientType clientType, String videoId, private static HttpURLConnection send(ClientType clientType,
String videoId,
Map<String, String> playerHeaders, Map<String, String> playerHeaders,
boolean showErrorToasts) { boolean showErrorToasts) {
Objects.requireNonNull(clientType); Objects.requireNonNull(clientType);
@ -128,21 +129,24 @@ public class StreamingDataRequest {
Objects.requireNonNull(playerHeaders); Objects.requireNonNull(playerHeaders);
final long startTime = System.currentTimeMillis(); final long startTime = System.currentTimeMillis();
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
try { try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
boolean authHeadersIncludes = false;
for (String key : REQUEST_HEADER_KEYS) { for (String key : REQUEST_HEADER_KEYS) {
String value = playerHeaders.get(key); String value = playerHeaders.get(key);
if (value != null) { if (value != null) {
if (key.equals(AUTHORIZATION_HEADER)) { if (key.equals(AUTHORIZATION_HEADER)) {
if (!clientType.canLogin) { if (!clientType.useAuth) {
Logger.printDebug(() -> "Not including request header: " + key); Logger.printDebug(() -> "Not including request header: " + key);
continue; continue;
} }
authHeadersIncludes = true;
} }
Logger.printDebug(() -> "Including request header: " + key); 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); byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
connection.setFixedLengthStreamingMode(requestBody.length); connection.setFixedLengthStreamingMode(requestBody.length);
connection.getOutputStream().write(requestBody); connection.getOutputStream().write(requestBody);