diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java
index ab3a25dc2..edad594b4 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java
@@ -1,19 +1,44 @@
package app.revanced.extension.music.patches.misc;
-import app.revanced.extension.music.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.music.settings.Settings;
@SuppressWarnings("unused")
public class SpoofClientPatch {
+ private static final int CLIENT_ID_IOS_MUSIC = 26;
+ /**
+ * 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 CLIENT_VERSION_IOS_MUSIC = "6.21";
+ /**
+ * See this GitHub Gist for more
+ * information.
+ *
+ */
+ private static final String DEVICE_MODEL_IOS_MUSIC = "iPhone16,2";
+ private static final String OS_VERSION_IOS_MUSIC = "17.7.2.21H221";
+ private static final String USER_AGENT_VERSION_IOS_MUSIC = "17_7_2";
+ private static final String USER_AGENT_IOS_MUSIC = "com.google.ios.youtubemusic/" +
+ CLIENT_VERSION_IOS_MUSIC +
+ "(" +
+ DEVICE_MODEL_IOS_MUSIC +
+ "; U; CPU iOS " +
+ USER_AGENT_VERSION_IOS_MUSIC +
+ " like Mac OS X)";
+
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
- private static final ClientType clientType = ClientType.IOS_MUSIC;
/**
* Injection point.
*/
public static int getClientTypeId(int originalClientTypeId) {
if (SPOOF_CLIENT_ENABLED) {
- return clientType.id;
+ return CLIENT_ID_IOS_MUSIC;
}
return originalClientTypeId;
@@ -24,7 +49,7 @@ public class SpoofClientPatch {
*/
public static String getClientVersion(String originalClientVersion) {
if (SPOOF_CLIENT_ENABLED) {
- return clientType.clientVersion;
+ return CLIENT_VERSION_IOS_MUSIC;
}
return originalClientVersion;
@@ -35,7 +60,7 @@ public class SpoofClientPatch {
*/
public static String getClientModel(String originalClientModel) {
if (SPOOF_CLIENT_ENABLED) {
- return clientType.deviceModel;
+ return DEVICE_MODEL_IOS_MUSIC;
}
return originalClientModel;
@@ -46,7 +71,7 @@ public class SpoofClientPatch {
*/
public static String getOsVersion(String originalOsVersion) {
if (SPOOF_CLIENT_ENABLED) {
- return clientType.osVersion;
+ return OS_VERSION_IOS_MUSIC;
}
return originalOsVersion;
@@ -57,7 +82,7 @@ public class SpoofClientPatch {
*/
public static String getUserAgent(String originalUserAgent) {
if (SPOOF_CLIENT_ENABLED) {
- return clientType.userAgent;
+ return USER_AGENT_IOS_MUSIC;
}
return originalUserAgent;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java
deleted file mode 100644
index 684c8b0b0..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package app.revanced.extension.music.patches.misc.client;
-
-import android.os.Build;
-
-import androidx.annotation.Nullable;
-
-public class AppClient {
-
- /**
- * 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 CLIENT_VERSION_IOS = "6.21";
- private static final String DEVICE_MAKE_IOS = "Apple";
- /**
- * See this GitHub Gist for more
- * information.
- *
- */
- private static final String DEVICE_MODEL_IOS = "iPhone16,2";
- private static final String OS_NAME_IOS = "iOS";
- private static final String OS_VERSION_IOS = "17.7.2.21H221";
- private static final String USER_AGENT_VERSION_IOS = "17_7_2";
- private static final String USER_AGENT_IOS = "com.google.ios.youtubemusic/" +
- CLIENT_VERSION_IOS +
- "(" +
- DEVICE_MODEL_IOS +
- "; U; CPU iOS " +
- USER_AGENT_VERSION_IOS +
- " like Mac OS X)";
-
- private AppClient() {
- }
-
- public enum ClientType {
- IOS_MUSIC(26,
- DEVICE_MAKE_IOS,
- DEVICE_MODEL_IOS,
- CLIENT_VERSION_IOS,
- OS_NAME_IOS,
- OS_VERSION_IOS,
- null,
- USER_AGENT_IOS,
- true
- );
-
- /**
- * YouTube
- * client type
- */
- public final int id;
-
- /**
- * Device manufacturer.
- */
- @Nullable
- public final String deviceMake;
-
- /**
- * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
- */
- public final String deviceModel;
-
- /**
- * Device OS name.
- */
- @Nullable
- public final String osName;
-
- /**
- * Device OS version.
- */
- 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.
- */
- public final Integer androidSdkVersion;
-
- /**
- * App version.
- */
- public final String clientVersion;
-
- /**
- * If the client can access the API logged in.
- */
- public final boolean canLogin;
-
- ClientType(int id,
- @Nullable String deviceMake,
- String deviceModel,
- String clientVersion,
- @Nullable String osName,
- String osVersion,
- Integer androidSdkVersion,
- String userAgent,
- boolean canLogin
- ) {
- this.id = id;
- this.deviceMake = deviceMake;
- this.deviceModel = deviceModel;
- this.clientVersion = clientVersion;
- this.osName = osName;
- this.osVersion = osVersion;
- this.androidSdkVersion = androidSdkVersion;
- this.userAgent = userAgent;
- this.canLogin = canLogin;
- }
- }
-}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java
index d1065c5ba..5006cb9cd 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java
@@ -1,8 +1,14 @@
package app.revanced.extension.shared.patches;
+import app.revanced.extension.shared.patches.client.AppClient.ClientType;
+
@SuppressWarnings("unused")
public class PatchStatus {
public static boolean HideFullscreenAdsDefaultBoolean() {
return false;
}
+
+ public static ClientType SpoofStreamingDataDefaultClient() {
+ return ClientType.IOS;
+ }
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/AppClient.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java
similarity index 74%
rename from extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/AppClient.java
rename to extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java
index 25c49ae86..80033c845 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/AppClient.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java
@@ -1,16 +1,12 @@
-package app.revanced.extension.youtube.patches.misc.client;
+package app.revanced.extension.shared.patches.client;
-import static app.revanced.extension.shared.utils.StringRef.str;
+import static app.revanced.extension.shared.utils.ResourceUtils.getString;
import android.os.Build;
import androidx.annotation.Nullable;
public class AppClient {
-
- // ANDROID
- private static final String OS_NAME_ANDROID = "Android";
-
// IOS
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
@@ -21,8 +17,7 @@ public class AppClient {
* Store page of the YouTube app, in the {@code What’s New} section.
*
*/
- private static final String CLIENT_VERSION_IOS = "19.47.7";
- private static final String DEVICE_MAKE_IOS = "Apple";
+ private static final String CLIENT_VERSION_IOS = "19.49.5";
/**
* The device machine id for the iPhone 16 Pro Max (iPhone17,2), used to get HDR with AV1 hardware decoding.
*
@@ -31,11 +26,13 @@ public class AppClient {
* information.
*
*/
- private static final String DEVICE_MODEL_IOS = "iPhone17,2"
- private static final String OS_NAME_IOS = "iOS";
- private static final String OS_VERSION_IOS = "18.1.1.22B91"
- private static final String USER_AGENT_VERSION_IOS = "18_1_1"
-
+ private static final String DEVICE_MODEL_IOS = "iPhone17,2";
+ /**
+ * 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 OS_VERSION_IOS = "18.1.1.22B91";
+ private static final String USER_AGENT_VERSION_IOS = "18_1_1";
private static final String USER_AGENT_IOS = "com.google.ios.youtube/" +
CLIENT_VERSION_IOS +
"(" +
@@ -44,6 +41,25 @@ public class AppClient {
USER_AGENT_VERSION_IOS +
" like Mac OS X)";
+ // IOS_MUSIC
+ /**
+ * 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 CLIENT_VERSION_IOS_MUSIC = "7.31.2";
+ private static final String USER_AGENT_IOS_MUSIC = "com.google.ios.youtubemusic/" +
+ CLIENT_VERSION_IOS +
+ "(" +
+ DEVICE_MODEL_IOS +
+ "; U; CPU iOS " +
+ USER_AGENT_VERSION_IOS +
+ " like Mac OS X)";
+
// ANDROID VR
/**
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
@@ -54,7 +70,7 @@ public class AppClient {
* Store page of the YouTube app, in the {@code Additional details} section.
*
*/
- private static final String CLIENT_VERSION_ANDROID_VR = "1.60.19";
+ private static final String CLIENT_VERSION_ANDROID_VR = "1.61.47";
/**
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
*
@@ -69,7 +85,7 @@ public class AppClient {
* 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_SDK_VERSION_ANDROID_VR = 32;
+ private static final String ANDROID_SDK_VERSION_ANDROID_VR = "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
@@ -82,7 +98,7 @@ public class AppClient {
"; GB) gzip";
// ANDROID UNPLUGGED
- private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.47.0";
+ private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.49.0";
/**
* The device machine id for the Chromecast with Google TV 4K.
*
@@ -93,7 +109,7 @@ public class AppClient {
*/
private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer";
private static final String OS_VERSION_ANDROID_UNPLUGGED = "14";
- private static final int ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = 34;
+ private static final String ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34";
private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" +
CLIENT_VERSION_ANDROID_UNPLUGGED +
" (Linux; U; Android " +
@@ -105,61 +121,52 @@ public class AppClient {
public enum ClientType {
IOS(5,
- DEVICE_MAKE_IOS,
DEVICE_MODEL_IOS,
- CLIENT_VERSION_IOS,
- OS_NAME_IOS,
OS_VERSION_IOS,
- null,
USER_AGENT_IOS,
+ null,
+ CLIENT_VERSION_IOS,
false
),
ANDROID_VR(28,
- null,
DEVICE_MODEL_ANDROID_VR,
- CLIENT_VERSION_ANDROID_VR,
- OS_NAME_ANDROID,
OS_VERSION_ANDROID_VR,
- ANDROID_SDK_VERSION_ANDROID_VR,
USER_AGENT_ANDROID_VR,
+ ANDROID_SDK_VERSION_ANDROID_VR,
+ CLIENT_VERSION_ANDROID_VR,
true
),
ANDROID_UNPLUGGED(29,
- null,
DEVICE_MODEL_ANDROID_UNPLUGGED,
- CLIENT_VERSION_ANDROID_UNPLUGGED,
- OS_NAME_ANDROID,
OS_VERSION_ANDROID_UNPLUGGED,
- ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
USER_AGENT_ANDROID_UNPLUGGED,
+ ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
+ CLIENT_VERSION_ANDROID_UNPLUGGED,
+ true
+ ),
+ IOS_MUSIC(
+ 26,
+ DEVICE_MODEL_IOS,
+ OS_VERSION_IOS,
+ USER_AGENT_IOS_MUSIC,
+ null,
+ CLIENT_VERSION_IOS_MUSIC,
true
);
- public final String friendlyName;
-
/**
* YouTube
* client type
*/
public final int id;
- /**
- * Device manufacturer.
- */
- @Nullable
- public final String deviceMake;
+ public final String clientName;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String deviceModel;
- /**
- * Device OS name.
- */
- @Nullable
- public final String osName;
-
/**
* Device OS version.
*/
@@ -174,7 +181,8 @@ public class AppClient {
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
* Field is null if not applicable.
*/
- public final Integer androidSdkVersion;
+ @Nullable
+ public final String androidSdkVersion;
/**
* App version.
@@ -187,25 +195,25 @@ public class AppClient {
public final boolean canLogin;
ClientType(int id,
- @Nullable String deviceMake,
String deviceModel,
- String clientVersion,
- @Nullable String osName,
String osVersion,
- Integer androidSdkVersion,
String userAgent,
+ @Nullable String androidSdkVersion,
+ String clientVersion,
boolean canLogin
) {
- this.friendlyName = str("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
this.id = id;
- this.deviceMake = deviceMake;
+ this.clientName = name();
this.deviceModel = deviceModel;
this.clientVersion = clientVersion;
- this.osName = osName;
this.osVersion = osVersion;
this.androidSdkVersion = androidSdkVersion;
this.userAgent = userAgent;
this.canLogin = canLogin;
}
+
+ public final String getFriendlyName() {
+ return getString("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
+ }
}
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java
similarity index 82%
rename from extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/SpoofStreamingDataPatch.java
rename to extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java
index 1030707d7..21a21379f 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/SpoofStreamingDataPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java
@@ -1,4 +1,4 @@
-package app.revanced.extension.youtube.patches.misc;
+package app.revanced.extension.shared.patches.spoof;
import android.net.Uri;
import android.text.TextUtils;
@@ -7,19 +7,15 @@ import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Map;
-import java.util.Objects;
-import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
-import app.revanced.extension.youtube.patches.misc.requests.StreamingDataRequest;
-import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch {
- private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_STREAMING_DATA.get();
-
+ private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_STREAMING_DATA.get();
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
@@ -90,10 +86,19 @@ public class SpoofStreamingDataPatch {
try {
Uri uri = Uri.parse(url);
String path = uri.getPath();
+
// 'heartbeat' has no video id and appears to be only after playback has started.
- if (path != null && path.contains("player") && !path.contains("heartbeat")) {
- String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
- StreamingDataRequest.fetchRequest(videoId, requestHeaders);
+ // 'refresh' has no video id and appears to happen when waiting for a livestream to start.
+ if (path != null && path.contains("player") && !path.contains("heartbeat")
+ && !path.contains("refresh")) {
+ String id = uri.getQueryParameter("id");
+ if (id == null) {
+ Logger.printException(() -> "Ignoring request that has no video id." +
+ " Url: " + url + " headers: " + requestHeaders);
+ return;
+ }
+
+ StreamingDataRequest.fetchRequest(id, requestHeaders);
}
} catch (Exception ex) {
Logger.printException(() -> "buildRequest failure", ex);
@@ -104,7 +109,7 @@ public class SpoofStreamingDataPatch {
/**
* Injection point.
* Fix playback by replace the streaming data.
- * Called after {@link #fetchStreams(String, Map)} .
+ * Called after {@link #fetchStreams(String, Map)}.
*/
@Nullable
public static ByteBuffer getStreamingData(String videoId) {
@@ -117,9 +122,10 @@ public class SpoofStreamingDataPatch {
// This is not a concern, since the fetch will always be finished
// and never block the main thread.
// But if debugging, then still verify this is the situation.
- if (Settings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
+ if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
Logger.printException(() -> "Error: Blocking main thread");
}
+
var stream = request.getStream();
if (stream != null) {
Logger.printDebug(() -> "Overriding video stream: " + videoId);
@@ -164,7 +170,7 @@ public class SpoofStreamingDataPatch {
*/
public static String appendSpoofedClient(String videoFormat) {
try {
- if (SPOOF_STREAMING_DATA && Settings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
+ if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_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)", StreamingDataRequest.getLastSpoofedClientName()); // u202D = left to right override
@@ -175,11 +181,4 @@ public class SpoofStreamingDataPatch {
return videoFormat;
}
-
- public static final class iOSAvailability implements Setting.Availability {
- @Override
- public boolean isAvailable() {
- return Settings.SPOOF_STREAMING_DATA.get() && Settings.SPOOF_STREAMING_DATA_TYPE.get() == ClientType.IOS;
- }
- }
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlayerRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java
similarity index 57%
rename from extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlayerRoutes.java
rename to extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java
index 5a7f87c94..f42e5443c 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlayerRoutes.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java
@@ -1,81 +1,69 @@
-package app.revanced.extension.youtube.patches.misc.requests;
+package app.revanced.extension.shared.patches.spoof.requests;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
-import java.util.Objects;
+import app.revanced.extension.shared.patches.client.AppClient.ClientType;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
-@SuppressWarnings("deprecation")
+@SuppressWarnings({"ExtractMethodRecommender", "deprecation"})
public final class PlayerRoutes {
- /**
- * The base URL of requests of non-web clients to the InnerTube internal API.
- */
- private static final String YOUTUBEI_V1_GAPIS_URL = "https://youtubei.googleapis.com/youtubei/v1/";
-
+ public static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route(
+ Route.Method.POST,
+ "next" +
+ "?fields=contents.singleColumnWatchNextResults.playlist.playlist"
+ ).compile();
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
Route.Method.POST,
"player" +
"?fields=streamingData" +
"&alt=proto"
).compile();
-
- static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route(
- Route.Method.POST,
- "next" +
- "?fields=contents.singleColumnWatchNextResults.playlist.playlist"
- ).compile();
-
+ private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
/**
* TCP connection and HTTP read timeout
*/
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
+ private static final String LOCALE_LANGUAGE = Utils.getContext().getResources()
+ .getConfiguration().locale.getLanguage();
+
private PlayerRoutes() {
}
- static String createInnertubeBody(ClientType clientType, String videoId) {
- return createInnertubeBody(clientType, videoId, null);
+ public static String createInnertubeBody(ClientType clientType) {
+ return createInnertubeBody(clientType, false);
}
- static String createInnertubeBody(ClientType clientType, String videoId, String playlistId) {
+ public static String createInnertubeBody(ClientType clientType, boolean playlistId) {
JSONObject innerTubeBody = new JSONObject();
try {
- JSONObject context = new JSONObject();
-
JSONObject client = new JSONObject();
- client.put("clientName", clientType.name());
+ client.put("clientName", clientType.clientName);
client.put("clientVersion", clientType.clientVersion);
client.put("deviceModel", clientType.deviceModel);
client.put("osVersion", clientType.osVersion);
- if (clientType.deviceMake != null) {
- client.put("deviceMake", clientType.deviceMake);
- }
- if (clientType.osName != null) {
- client.put("osName", clientType.osName);
- }
if (clientType.androidSdkVersion != null) {
- client.put("androidSdkVersion", clientType.androidSdkVersion.toString());
+ client.put("androidSdkVersion", clientType.androidSdkVersion);
}
- String languageCode = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale.getLanguage();
- client.put("hl", languageCode);
+ client.put("hl", LOCALE_LANGUAGE);
+ JSONObject context = new JSONObject();
context.put("client", client);
innerTubeBody.put("context", context);
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
- innerTubeBody.put("videoId", videoId);
- if (playlistId != null) {
- innerTubeBody.put("playlistId", playlistId);
+ innerTubeBody.put("videoId", "%s");
+ if (playlistId) {
+ innerTubeBody.put("playlistId", "%s");
}
} catch (JSONException e) {
Logger.printException(() -> "Failed to create innerTubeBody", e);
@@ -87,13 +75,11 @@ public final class PlayerRoutes {
/**
* @noinspection SameParameterValue
*/
- static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
- var connection = Requester.getConnectionFromCompiledRoute(YOUTUBEI_V1_GAPIS_URL, route);
+ public static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
+ var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", clientType.userAgent);
- connection.setRequestProperty("X-YouTube-Client-Name", clientType.id);
- connection.setRequestProperty("X-YouTube-Client-Version", clientType.clientVersion);
connection.setUseCaches(false);
connection.setDoOutput(true);
@@ -102,4 +88,4 @@ public final class PlayerRoutes {
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
return connection;
}
-}
+}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java
similarity index 60%
rename from extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/StreamingDataRequest.java
rename to extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java
index f3a2479ec..0e9f8c76b 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/StreamingDataRequest.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java
@@ -1,8 +1,7 @@
-package app.revanced.extension.youtube.patches.misc.requests;
+package app.revanced.extension.shared.patches.spoof.requests;
-import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA;
+import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
-import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -11,60 +10,57 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
+import app.revanced.extension.shared.patches.client.AppClient.ClientType;
+
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
-import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.shared.settings.BaseSettings;
+/**
+ * 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
+ * did use its own client streams.
+ */
public class StreamingDataRequest {
- private static final ClientType[] ALL_CLIENT_TYPES = ClientType.values();
+
private static final ClientType[] CLIENT_ORDER_TO_USE;
-
- static {
- ClientType preferredClient = Settings.SPOOF_STREAMING_DATA_TYPE.get();
- CLIENT_ORDER_TO_USE = new ClientType[ALL_CLIENT_TYPES.length];
-
- CLIENT_ORDER_TO_USE[0] = preferredClient;
-
- int i = 1;
- for (ClientType c : ALL_CLIENT_TYPES) {
- if (c != preferredClient) {
- CLIENT_ORDER_TO_USE[i++] = c;
- }
- }
- }
-
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private static final String[] REQUEST_HEADER_KEYS = {
+ AUTHORIZATION_HEADER, // Available only to logged-in users.
+ "X-GOOG-API-FORMAT-VERSION",
+ "X-Goog-Visitor-Id"
+ };
+ private static final ByteArrayFilterGroup liveStreams =
+ new ByteArrayFilterGroup(
+ BaseSettings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK,
+ "yt_live_broadcast",
+ "yt_premiere_broadcast"
+ );
private static ClientType lastSpoofedClientType;
- public static String getLastSpoofedClientName() {
- return lastSpoofedClientType == null
- ? "Unknown"
- : lastSpoofedClientType.friendlyName;
- }
/**
* 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;
-
- @GuardedBy("itself")
private static final Map cache = Collections.synchronizedMap(
new LinkedHashMap<>(100) {
/**
@@ -82,12 +78,43 @@ public class StreamingDataRequest {
}
});
- public static void fetchRequest(@NonNull String videoId, Map fetchHeaders) {
+ public static String getLastSpoofedClientName() {
+ return lastSpoofedClientType == null
+ ? "Unknown"
+ : lastSpoofedClientType.getFriendlyName();
+ }
+
+ static {
+ ClientType[] allClientTypes = ClientType.values();
+ ClientType preferredClient = BaseSettings.SPOOF_STREAMING_DATA_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 an existing request for the same video.
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
}
@Nullable
- public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) {
+ public static StreamingDataRequest getRequestForVideoId(String videoId) {
return cache.get(videoId);
}
@@ -95,42 +122,6 @@ public class StreamingDataRequest {
Logger.printInfo(() -> toastMessage, ex);
}
- // Available only to logged in users.
- private static final String AUTHORIZATION_HEADER = "Authorization";
-
- private static final String[] REQUEST_HEADER_KEYS = {
- AUTHORIZATION_HEADER,
- "X-GOOG-API-FORMAT-VERSION",
- "X-Goog-Visitor-Id"
- };
-
- private static void writeInnerTubeBody(HttpURLConnection connection, ClientType clientType,
- String videoId, Map playerHeaders) {
- try {
- connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
- connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
-
- if (playerHeaders != null) {
- 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);
- }
- }
- }
-
- String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId);
- byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
- connection.setFixedLengthStreamingMode(requestBody.length);
- connection.getOutputStream().write(requestBody);
- } catch (IOException ex) {
- handleConnectionError("Network error", ex);
- }
- }
-
@Nullable
private static HttpURLConnection send(ClientType clientType, String videoId,
Map playerHeaders) {
@@ -139,19 +130,44 @@ public class StreamingDataRequest {
Objects.requireNonNull(playerHeaders);
final long startTime = System.currentTimeMillis();
- String clientTypeName = clientType.name();
- Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
+ Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
- writeInnerTubeBody(connection, clientType, videoId, playerHeaders);
+ connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
+ connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
+
+ for (String key : REQUEST_HEADER_KEYS) {
+ String value = playerHeaders.get(key);
+ if (value != null) {
+ if (key.equals(AUTHORIZATION_HEADER)) {
+ if (!clientType.canLogin) {
+ Logger.printDebug(() -> "Not including request header: " + key);
+ continue;
+ }
+ }
+
+ connection.setRequestProperty(key, value);
+ }
+ }
+
+ String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
+ byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
+ connection.setFixedLengthStreamingMode(requestBody.length);
+ connection.getOutputStream().write(requestBody);
final int responseCode = connection.getResponseCode();
if (responseCode == 200) return connection;
- handleConnectionError(clientTypeName + " not available with response code: "
- + responseCode + " message: " + connection.getResponseMessage(),
+ // This situation likely means the patches are outdated.
+ // Use a toast message that suggests updating.
+ handleConnectionError("Playback error (App is outdated?) " + clientType + ": "
+ + responseCode + " response: " + connection.getResponseMessage(),
null);
+ } catch (SocketTimeoutException ex) {
+ handleConnectionError("Connection timeout", ex);
+ } catch (IOException ex) {
+ handleConnectionError("Network error", ex);
} catch (Exception ex) {
Logger.printException(() -> "send failed", ex);
} finally {
@@ -161,43 +177,39 @@ public class StreamingDataRequest {
return null;
}
- private static final ByteArrayFilterGroup liveStreams =
- new ByteArrayFilterGroup(
- Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK,
- "yt_live_broadcast",
- "yt_premiere_broadcast"
- );
-
- private static ByteBuffer fetch(@NonNull String videoId, Map playerHeaders) {
+ private static ByteBuffer fetch(String videoId, Map playerHeaders) {
lastSpoofedClientType = null;
// Retry with different client if empty response body is received.
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
HttpURLConnection connection = send(clientType, videoId, playerHeaders);
+ if (connection != null) {
+ try {
+ // gzip encoding doesn't response with content length (-1),
+ // but empty response body does.
+ if (connection.getContentLength() == 0) {
+ Logger.printDebug(() -> "Received empty response" + "\nClient: " + clientType + "\nVideo: " + videoId);
+ } else {
+ try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- // gzip encoding doesn't response with content length (-1),
- // but empty response body does.
- if (connection == null || connection.getContentLength() == 0)
- continue;
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) >= 0) {
+ baos.write(buffer, 0, bytesRead);
+ }
+ if (clientType == ClientType.IOS && liveStreams.check(buffer).isFiltered()) {
+ Logger.printDebug(() -> "Ignore IOS spoofing as it is a livestream (video: " + videoId + ")");
+ continue;
+ }
+ lastSpoofedClientType = clientType;
- try (
- InputStream inputStream = new BufferedInputStream(connection.getInputStream());
- ByteArrayOutputStream baos = new ByteArrayOutputStream()
- ) {
- byte[] buffer = new byte[2048];
- int bytesRead;
- while ((bytesRead = inputStream.read(buffer)) >= 0) {
- baos.write(buffer, 0, bytesRead);
+ return ByteBuffer.wrap(baos.toByteArray());
+ }
+ }
+ } catch (IOException ex) {
+ Logger.printException(() -> "Fetch failed while processing response data", ex);
}
- if (clientType == ClientType.IOS && liveStreams.check(buffer).isFiltered()) {
- Logger.printDebug(() -> "Ignore IOS spoofing as it is a livestream (video: " + videoId + ")");
- continue;
- }
- lastSpoofedClientType = clientType;
-
- return ByteBuffer.wrap(baos.toByteArray());
- } catch (IOException ex) {
- Logger.printException(() -> "Fetch failed while processing response data", ex);
}
}
@@ -205,15 +217,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();
}
@@ -239,4 +242,4 @@ public class StreamingDataRequest {
public String toString() {
return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
}
-}
\ No newline at end of file
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java
index 8ab950f25..831b8bf63 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java
@@ -11,9 +11,11 @@ import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
+import app.revanced.extension.shared.utils.PackageUtils;
+
@SuppressWarnings("unused")
public class Requester {
- public Requester() {
+ private Requester() {
}
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
@@ -27,7 +29,9 @@ public class Requester {
// The calling code must set a length if using a request body.
connection.setFixedLengthStreamingMode(0);
connection.setRequestMethod(route.getMethod().name());
- connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + ";");
+ String agentString = System.getProperty("http.agent")
+ + "; RVX/" + PackageUtils.getAppVersionName();
+ connection.setRequestProperty("User-Agent", agentString);
return connection;
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Route.java b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Route.java
index 9ce0c7654..9e6f2c5a7 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Route.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Route.java
@@ -2,10 +2,10 @@ package app.revanced.extension.shared.requests;
public class Route {
private final String route;
- private final Route.Method method;
+ private final Method method;
private final int paramCount;
- public Route(Route.Method method, String route) {
+ public Route(Method method, String route) {
this.method = method;
this.route = route;
this.paramCount = countMatches(route, '{');
@@ -14,11 +14,11 @@ public class Route {
throw new IllegalArgumentException("Not enough parameters");
}
- public Route.Method getMethod() {
+ public Method getMethod() {
return method;
}
- public Route.CompiledRoute compile(String... params) {
+ public CompiledRoute compile(String... params) {
if (params.length != paramCount)
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
"Expected: " + paramCount + ", provided: " + params.length);
@@ -29,21 +29,7 @@ public class Route {
int paramEnd = compiledRoute.indexOf("}");
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
}
- return new Route.CompiledRoute(this, compiledRoute.toString());
- }
-
- private int countMatches(CharSequence seq, char c) {
- int count = 0;
- for (int i = 0; i < seq.length(); i++) {
- if (seq.charAt(i) == c)
- count++;
- }
- return count;
- }
-
- public enum Method {
- GET,
- POST
+ return new CompiledRoute(this, compiledRoute.toString());
}
public static class CompiledRoute {
@@ -59,8 +45,22 @@ public class Route {
return compiledRoute;
}
- public Route.Method getMethod() {
+ public Method getMethod() {
return baseRoute.method;
}
}
+
+ private int countMatches(CharSequence seq, char c) {
+ int count = 0;
+ for (int i = 0; i < seq.length(); i++) {
+ if (seq.charAt(i) == c)
+ count++;
+ }
+ return count;
+ }
+
+ public enum Method {
+ GET,
+ POST
+ }
}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
index 5ef848c54..763046b85 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
@@ -4,8 +4,10 @@ import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean;
+import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataDefaultClient;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
+import app.revanced.extension.shared.patches.client.AppClient.ClientType;
/**
* Settings shared across multiple apps.
@@ -35,6 +37,11 @@ public class BaseSettings {
public static final EnumSetting RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = new EnumSetting<>("revanced_return_youtube_username_display_format", DisplayFormat.USERNAME_ONLY, true);
public static final StringSetting RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY = new StringSetting("revanced_return_youtube_username_youtube_data_api_v3_developer_key", "", true, false);
+ public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
+ public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", SpoofStreamingDataDefaultClient(), true);
+ public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK = new BooleanSetting("revanced_spoof_streaming_data_ios_skip_livestream_playback", TRUE, true);
+ public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
+
/**
* @noinspection DeprecatedIsStillUsed
*/
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
index 6964d3625..99b658d0a 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
@@ -9,8 +9,8 @@ import org.apache.commons.lang3.BooleanUtils;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.patches.misc.requests.PlaylistRequest;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
+import app.revanced.extension.youtube.patches.video.requests.PlaylistRequest;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.whitelist.Whitelist;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlaylistRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java
similarity index 94%
rename from extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlaylistRequest.java
rename to extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java
index 370e23cfc..b7c69cd0a 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlaylistRequest.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java
@@ -1,6 +1,6 @@
-package app.revanced.extension.youtube.patches.misc.requests;
+package app.revanced.extension.youtube.patches.video.requests;
-import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_PLAYLIST_PAGE;
+import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_PLAYLIST_PAGE;
import android.annotation.SuppressLint;
@@ -16,6 +16,7 @@ import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
@@ -23,10 +24,11 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import app.revanced.extension.shared.patches.client.AppClient.ClientType;
+import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.shared.VideoInformation;
public class PlaylistRequest {
@@ -82,8 +84,9 @@ public class PlaylistRequest {
try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType);
- String innerTubeBody = PlayerRoutes.createInnertubeBody(
- clientType,
+ String innerTubeBody = String.format(
+ Locale.ENGLISH,
+ PlayerRoutes.createInnertubeBody(clientType, true),
videoId,
"RD" + videoId
);
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 1a55ee198..24a05fe35 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -37,9 +37,7 @@ import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.Start
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
import app.revanced.extension.youtube.patches.general.MiniplayerPatch;
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
-import app.revanced.extension.youtube.patches.misc.SpoofStreamingDataPatch;
import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
-import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
@@ -563,11 +561,6 @@ public class Settings extends BaseSettings {
public static final EnumSetting WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE);
// PreferenceScreen: Miscellaneous - Spoof streaming data
- // The order of the settings should not be changed otherwise the app may crash
- public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
- public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK = new BooleanSetting("revanced_spoof_streaming_data_ios_skip_livestream_playback", TRUE, true, new SpoofStreamingDataPatch.iOSAvailability());
- public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.IOS, true, parent(SPOOF_STREAMING_DATA));
- public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_STREAMING_DATA));
// PreferenceScreen: Return YouTube Dislike
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
index 3ada4f0ad..40b64919d 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
@@ -8,9 +8,9 @@ import android.preference.Preference;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
+import app.revanced.extension.shared.patches.client.AppClient.ClientType;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "unused"})
@@ -74,5 +74,6 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
setSummary(str(summaryTextKey));
setEnabled(Settings.SPOOF_STREAMING_DATA.get());
+ setSelectable(false);
}
}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
index 8ef58e42f..5b3cfe920 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
@@ -46,6 +46,7 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
val spoofClientPatch = bytecodePatch(
SPOOF_CLIENT.title,
SPOOF_CLIENT.summary,
+ false,
) {
dependsOn(settingsPatch)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
new file mode 100644
index 000000000..cbd66305e
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
@@ -0,0 +1,58 @@
+package app.revanced.patches.music.utils.fix.streamingdata
+
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME
+import app.revanced.patches.music.utils.patch.PatchList.SPOOF_STREAMING_DATA
+import app.revanced.patches.music.utils.settings.CategoryType
+import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
+import app.revanced.patches.music.utils.settings.addSwitchPreference
+import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
+import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
+import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
+import app.revanced.util.findMethodOrThrow
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+
+private const val DEFAULT_CLIENT_TYPE = "ANDROID_VR"
+
+val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
+ {
+ compatibleWith(COMPATIBLE_PACKAGE)
+
+ dependsOn(
+ baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME),
+ settingsPatch,
+ )
+ },
+ {
+ findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
+ name == "SpoofStreamingDataDefaultClient"
+ }.apply {
+ val register = getInstruction(0).registerA
+ val type = (getInstruction(0).reference as FieldReference).type
+ replaceInstruction(
+ 0,
+ "sget-object v$register, $type->$DEFAULT_CLIENT_TYPE:$type"
+ )
+ }
+
+ addSwitchPreference(
+ CategoryType.MISC,
+ "revanced_spoof_streaming_data",
+ "true"
+ )
+ addSwitchPreference(
+ CategoryType.MISC,
+ "revanced_spoof_streaming_data_stats_for_nerds",
+ "true",
+ "revanced_spoof_streaming_data"
+ )
+
+ updatePatchStatus(SPOOF_STREAMING_DATA)
+
+ }
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
index 704df7df5..c589f2660 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
@@ -143,7 +143,11 @@ internal enum class PatchList(
),
SPOOF_CLIENT(
"Spoof client",
- "Adds options to spoof the client to allow track playback."
+ "Adds options to spoof the client to allow playback."
+ ),
+ SPOOF_STREAMING_DATA(
+ "Spoof streaming data",
+ "Adds options to spoof the streaming data to allow playback."
),
TRANSLATIONS_FOR_YOUTUBE_MUSIC(
"Translations for YouTube Music",
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt
index 6229b593f..0d8eb94e4 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt
@@ -6,6 +6,7 @@ internal object Constants {
const val PATCHES_PATH = "$EXTENSION_PATH/patches"
const val COMPONENTS_PATH = "$PATCHES_PATH/components"
const val SPANS_PATH = "$PATCHES_PATH/spans"
+ const val SPOOF_PATH = "$PATCHES_PATH/spoof"
const val EXTENSION_UTILS_PATH = "$EXTENSION_PATH/utils"
const val EXTENSION_SETTING_CLASS_DESCRIPTOR = "$EXTENSION_PATH/settings/Setting;"
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt
new file mode 100644
index 000000000..a8beb08eb
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt
@@ -0,0 +1,215 @@
+package app.revanced.patches.shared.spoof.streamingdata
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.instructions
+import app.revanced.patcher.patch.BytecodePatchBuilder
+import app.revanced.patcher.patch.BytecodePatchContext
+import app.revanced.patcher.patch.PatchException
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.smali.ExternalLabel
+import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
+import app.revanced.util.findInstructionIndicesReversedOrThrow
+import app.revanced.util.fingerprint.definingClassOrThrow
+import app.revanced.util.fingerprint.matchOrThrow
+import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.getReference
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+
+const val EXTENSION_CLASS_DESCRIPTOR =
+ "$SPOOF_PATH/SpoofStreamingDataPatch;"
+
+fun baseSpoofStreamingDataPatch(
+ block: BytecodePatchBuilder.() -> Unit = {},
+ executeBlock: BytecodePatchContext.() -> Unit = {},
+) = bytecodePatch(
+ name = "Spoof streaming data",
+ description = "Adds options to spoof the streaming data to allow playback."
+) {
+ block()
+
+ execute {
+ // region Block /initplayback requests to fall back to /get_watch requests.
+
+ buildInitPlaybackRequestFingerprint.matchOrThrow().let {
+ it.method.apply {
+ val moveUriStringIndex = it.patternMatch!!.startIndex
+ val targetRegister =
+ getInstruction(moveUriStringIndex).registerA
+
+ addInstructions(
+ moveUriStringIndex + 1,
+ """
+ invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String;
+ move-result-object v$targetRegister
+ """,
+ )
+ }
+ }
+
+ // endregion
+
+ // region Block /get_watch requests to fall back to /player requests.
+
+ buildPlayerRequestURIFingerprint.methodOrThrow().apply {
+ val invokeToStringIndex = indexOfToStringInstruction(this)
+ val uriRegister =
+ getInstruction(invokeToStringIndex).registerC
+
+ addInstructions(
+ invokeToStringIndex,
+ """
+ invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
+ move-result-object v$uriRegister
+ """,
+ )
+ }
+
+ // endregion
+
+ // region Get replacement streams at player requests.
+
+ buildRequestFingerprint.methodOrThrow().apply {
+ val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this)
+ val urlRegister =
+ getInstruction(newRequestBuilderIndex).registerD
+
+ val entrySetIndex = indexOfEntrySetInstruction(this)
+ val mapRegister = if (entrySetIndex < 0)
+ urlRegister + 1
+ else
+ getInstruction(entrySetIndex).registerC
+
+ var smaliInstructions =
+ "invoke-static { v$urlRegister, v$mapRegister }, " +
+ "$EXTENSION_CLASS_DESCRIPTOR->" +
+ "fetchStreams(Ljava/lang/String;Ljava/util/Map;)V"
+
+ if (entrySetIndex < 0) smaliInstructions = """
+ move-object/from16 v$mapRegister, p1
+
+ """ + smaliInstructions
+
+ // Copy request headers for streaming data fetch.
+ addInstructions(newRequestBuilderIndex + 2, smaliInstructions)
+ }
+
+ // endregion
+
+ // region Replace the streaming data.
+
+ createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint).let { result ->
+ result.method.apply {
+ val setStreamingDataIndex = result.patternMatch!!.startIndex
+ val setStreamingDataField =
+ getInstruction(setStreamingDataIndex).getReference().toString()
+
+ val playerProtoClass =
+ getInstruction(setStreamingDataIndex + 1).getReference()!!.definingClass
+ val protobufClass =
+ protobufClassParseByteBufferFingerprint.definingClassOrThrow()
+
+ val getStreamingDataField = instructions.find { instruction ->
+ instruction.opcode == Opcode.IGET_OBJECT &&
+ instruction.getReference()?.definingClass == playerProtoClass
+ }?.getReference()
+ ?: throw PatchException("Could not find getStreamingDataField")
+
+ val videoDetailsIndex = result.patternMatch!!.endIndex
+ val videoDetailsClass =
+ getInstruction(videoDetailsIndex).getReference()!!.type
+
+ val insertIndex = videoDetailsIndex + 1
+ val videoDetailsRegister =
+ getInstruction(videoDetailsIndex).registerA
+
+ val overrideRegister = getInstruction(insertIndex).registerA
+ val freeRegister = implementation!!.registerCount - parameters.size - 2
+
+ addInstructionsWithLabels(
+ insertIndex,
+ """
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
+ move-result v$freeRegister
+ if-eqz v$freeRegister, :disabled
+
+ # Get video id.
+ # From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
+ iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String;
+ if-eqz v$freeRegister, :disabled
+
+ # Get streaming data.
+ invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
+ move-result-object v$freeRegister
+ if-eqz v$freeRegister, :disabled
+
+ # Parse streaming data.
+ sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass
+ invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
+ move-result-object v$freeRegister
+ check-cast v$freeRegister, $playerProtoClass
+
+ # Set streaming data.
+ iget-object v$freeRegister, v$freeRegister, $getStreamingDataField
+ if-eqz v$freeRegister, :disabled
+ iput-object v$freeRegister, p0, $setStreamingDataField
+
+ """,
+ ExternalLabel("disabled", getInstruction(insertIndex))
+ )
+ }
+ }
+
+ // endregion
+
+ // region Remove /videoplayback request body to fix playback.
+ // This is needed when using iOS client as streaming data source.
+
+ buildMediaDataSourceFingerprint.methodOrThrow().apply {
+ val targetIndex = instructions.lastIndex
+
+ addInstructions(
+ targetIndex,
+ """
+ # Field a: Stream uri.
+ # Field c: Http method.
+ # Field d: Post data.
+ move-object/from16 v0, p0
+ iget-object v1, v0, $definingClass->a:Landroid/net/Uri;
+ iget v2, v0, $definingClass->c:I
+ iget-object v3, v0, $definingClass->d:[B
+ invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B
+ move-result-object v1
+ iput-object v1, v0, $definingClass->d:[B
+ """,
+ )
+ }
+
+ // endregion
+
+ // region Append spoof info.
+
+ nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply {
+ findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
+ val register = getInstruction(index).registerA
+
+ addInstructions(
+ index, """
+ invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String;
+ move-result-object v$register
+ """
+ )
+ }
+ }
+
+ // endregion
+
+ executeBlock()
+
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt
similarity index 87%
rename from patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt
rename to patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt
index 79758b285..040316141 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.youtube.utils.fix.streamingdata
+package app.revanced.patches.shared.spoof.streamingdata
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
@@ -10,38 +10,6 @@ import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
-internal val buildBrowseRequestFingerprint = legacyFingerprint(
- name = "buildBrowseRequestFingerprint",
- customFingerprint = { method, _ ->
- method.implementation != null &&
- indexOfRequestFinishedListenerInstruction(method) >= 0 &&
- !method.definingClass.startsWith("Lorg/") &&
- indexOfNewUrlRequestBuilderInstruction(method) >= 0 &&
- // YouTube 17.34.36 ~ YouTube 18.35.36
- (indexOfEntrySetInstruction(method) >= 0 ||
- // YouTube 18.36.39 ~
- method.parameters[1].type == "Ljava/util/Map;")
- }
-)
-
-internal fun indexOfRequestFinishedListenerInstruction(method: Method) =
- method.indexOfFirstInstruction {
- opcode == Opcode.INVOKE_VIRTUAL &&
- getReference()?.name == "setRequestFinishedListener"
- }
-
-internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
- method.indexOfFirstInstruction {
- opcode == Opcode.INVOKE_VIRTUAL &&
- getReference().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;"
- }
-
-internal fun indexOfEntrySetInstruction(method: Method) =
- method.indexOfFirstInstruction {
- opcode == Opcode.INVOKE_INTERFACE &&
- getReference().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
- }
-
internal val buildInitPlaybackRequestFingerprint = legacyFingerprint(
name = "buildInitPlaybackRequestFingerprint",
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
@@ -91,6 +59,38 @@ internal fun indexOfToStringInstruction(method: Method) =
getReference().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;"
}
+internal val buildRequestFingerprint = legacyFingerprint(
+ name = "buildRequestFingerprint",
+ customFingerprint = { method, _ ->
+ method.implementation != null &&
+ indexOfRequestFinishedListenerInstruction(method) >= 0 &&
+ !method.definingClass.startsWith("Lorg/") &&
+ indexOfNewUrlRequestBuilderInstruction(method) >= 0 &&
+ // Earlier targets
+ (indexOfEntrySetInstruction(method) >= 0 ||
+ // Later targets
+ method.parameters[1].type == "Ljava/util/Map;")
+ }
+)
+
+internal fun indexOfRequestFinishedListenerInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.name == "setRequestFinishedListener"
+ }
+
+internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;"
+ }
+
+internal fun indexOfEntrySetInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ opcode == Opcode.INVOKE_INTERFACE &&
+ getReference().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
+ }
+
internal val createStreamingDataFingerprint = legacyFingerprint(
name = "createStreamingDataFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
@@ -103,19 +103,21 @@ internal val createStreamingDataFingerprint = legacyFingerprint(
Opcode.SGET_OBJECT,
Opcode.IPUT_OBJECT
),
- customFingerprint = { method, _ ->
- method.indexOfFirstInstruction {
- opcode == Opcode.SGET_OBJECT &&
- getReference()?.name == "playerThreedRenderer"
- } >= 0
- },
+)
+
+internal val createStreamingDataParentFingerprint = legacyFingerprint(
+ name = "createStreamingDataParentFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ returnType = "L",
+ parameters = emptyList(),
+ strings = listOf("Invalid playback type; streaming data is not playable"),
)
internal val nerdsStatsVideoFormatBuilderFingerprint = legacyFingerprint(
name = "nerdsStatsVideoFormatBuilderFingerprint",
returnType = "Ljava/lang/String;",
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
- parameters = listOf("Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"),
+ parameters = listOf("L"),
strings = listOf("codecs=\""),
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
index 0a7ddf7a6..a2ba66890 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
@@ -1,222 +1,23 @@
package app.revanced.patches.youtube.utils.fix.streamingdata
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
-import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.instructions
-import app.revanced.patcher.patch.PatchException
-import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patcher.util.smali.ExternalLabel
+import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
-import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH
+import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME
import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
-import app.revanced.util.findInstructionIndicesReversedOrThrow
-import app.revanced.util.fingerprint.definingClassOrThrow
-import app.revanced.util.fingerprint.matchOrThrow
-import app.revanced.util.fingerprint.methodOrThrow
-import app.revanced.util.getReference
-import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.reference.FieldReference
-private const val EXTENSION_CLASS_DESCRIPTOR =
- "$MISC_PATH/SpoofStreamingDataPatch;"
-
-val spoofStreamingDataPatch = bytecodePatch(
- SPOOF_STREAMING_DATA.title,
- SPOOF_STREAMING_DATA.summary,
-) {
- compatibleWith(COMPATIBLE_PACKAGE)
-
- dependsOn(
- baseSpoofUserAgentPatch("com.google.android.youtube"),
- settingsPatch
- )
-
- execute {
- // region Block /get_watch requests to fall back to /player requests.
-
- buildPlayerRequestURIFingerprint.methodOrThrow().apply {
- val invokeToStringIndex = indexOfToStringInstruction(this)
- val uriRegister =
- getInstruction(invokeToStringIndex).registerC
-
- addInstructions(
- invokeToStringIndex,
- """
- invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
- move-result-object v$uriRegister
- """,
- )
- }
-
- // endregion
-
- // region Block /initplayback requests to fall back to /get_watch requests.
-
- buildInitPlaybackRequestFingerprint.matchOrThrow().let {
- it.method.apply {
- val moveUriStringIndex = it.patternMatch!!.startIndex
- val targetRegister =
- getInstruction(moveUriStringIndex).registerA
-
- addInstructions(
- moveUriStringIndex + 1,
- """
- invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String;
- move-result-object v$targetRegister
- """,
- )
- }
- }
-
- // endregion
-
- // region Fetch replacement streams.
-
- buildBrowseRequestFingerprint.methodOrThrow().apply {
- val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this)
- val urlRegister =
- getInstruction(newRequestBuilderIndex).registerD
-
- val entrySetIndex = indexOfEntrySetInstruction(this)
- val mapRegister = if (entrySetIndex < 0)
- urlRegister + 1
- else
- getInstruction(entrySetIndex).registerC
-
- var smaliInstructions =
- "invoke-static { v$urlRegister, v$mapRegister }, " +
- "$EXTENSION_CLASS_DESCRIPTOR->" +
- "fetchStreams(Ljava/lang/String;Ljava/util/Map;)V"
-
- if (entrySetIndex < 0) smaliInstructions = """
- move-object/from16 v$mapRegister, p1
-
- """ + smaliInstructions
-
- // Copy request headers for streaming data fetch.
- addInstructions(newRequestBuilderIndex + 2, smaliInstructions)
- }
-
- // endregion
-
- // region Replace the streaming data.
-
- createStreamingDataFingerprint.matchOrThrow().let { result ->
- result.method.apply {
- val setStreamingDataIndex = result.patternMatch!!.startIndex
- val setStreamingDataField =
- getInstruction(setStreamingDataIndex).getReference().toString()
-
- val playerProtoClass =
- getInstruction(setStreamingDataIndex + 1).getReference()!!.definingClass
- val protobufClass =
- protobufClassParseByteBufferFingerprint.definingClassOrThrow()
-
- val getStreamingDataField = instructions.find { instruction ->
- instruction.opcode == Opcode.IGET_OBJECT &&
- instruction.getReference()?.definingClass == playerProtoClass
- }?.getReference()
- ?: throw PatchException("Could not find getStreamingDataField")
-
- val videoDetailsIndex = result.patternMatch!!.endIndex
- val videoDetailsClass =
- getInstruction(videoDetailsIndex).getReference()!!.type
-
- val insertIndex = videoDetailsIndex + 1
- val videoDetailsRegister =
- getInstruction(videoDetailsIndex).registerA
-
- val overrideRegister = getInstruction(insertIndex).registerA
- val freeRegister = implementation!!.registerCount - parameters.size - 2
-
- addInstructionsWithLabels(
- insertIndex,
- """
- invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
- move-result v$freeRegister
- if-eqz v$freeRegister, :disabled
-
- # Get video id.
- # From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
- iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String;
- if-eqz v$freeRegister, :disabled
-
- # Get streaming data.
- invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
- move-result-object v$freeRegister
- if-eqz v$freeRegister, :disabled
-
- # Parse streaming data.
- sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass
- invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
- move-result-object v$freeRegister
- check-cast v$freeRegister, $playerProtoClass
-
- # Set streaming data.
- iget-object v$freeRegister, v$freeRegister, $getStreamingDataField
- if-eqz v$freeRegister, :disabled
- iput-object v$freeRegister, p0, $setStreamingDataField
-
- """,
- ExternalLabel("disabled", getInstruction(insertIndex))
- )
- }
- }
-
- // endregion
-
- // region Remove /videoplayback request body to fix playback.
- // This is needed when using iOS client as streaming data source.
-
- buildMediaDataSourceFingerprint.methodOrThrow().apply {
- val targetIndex = instructions.lastIndex
-
- addInstructions(
- targetIndex,
- """
- # Field a: Stream uri.
- # Field c: Http method.
- # Field d: Post data.
- # From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
- move-object/from16 v0, p0
- iget-object v1, v0, $definingClass->a:Landroid/net/Uri;
- iget v2, v0, $definingClass->c:I
- iget-object v3, v0, $definingClass->d:[B
- invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B
- move-result-object v1
- iput-object v1, v0, $definingClass->d:[B
- """,
- )
- }
-
- // endregion
-
- // region Append spoof info.
-
- nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply {
- findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
- val register = getInstruction(index).registerA
-
- addInstructions(
- index, """
- invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String;
- move-result-object v$register
- """
- )
- }
- }
-
- // endregion
-
- // region add settings
+val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
+ {
+ compatibleWith(COMPATIBLE_PACKAGE)
+ dependsOn(
+ baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME),
+ settingsPatch
+ )
+ },
+ {
addPreference(
arrayOf(
"SETTINGS: SPOOF_STREAMING_DATA"
@@ -224,7 +25,5 @@ val spoofStreamingDataPatch = bytecodePatch(
SPOOF_STREAMING_DATA
)
- // endregion
-
}
-}
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
index 8201842fb..5ebdc1893 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
@@ -223,7 +223,7 @@ internal enum class PatchList(
),
SPOOF_STREAMING_DATA(
"Spoof streaming data",
- "Adds options to spoof the streaming data to allow video playback."
+ "Adds options to spoof the streaming data to allow playback."
),
SWIPE_CONTROLS(
"Swipe controls",
diff --git a/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
index 9e73e6257..ef7e9dbcb 100644
--- a/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
+++ b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
@@ -55,6 +55,10 @@ internal fun Pair.matchOrNull(parentFingerprint: Pair.methodOrNull(): MutableMethod? =
+ matchOrNull()?.method
+
context(BytecodePatchContext)
internal fun Pair.methodOrThrow(): MutableMethod =
second.methodOrNull ?: throw first.exception
@@ -63,6 +67,14 @@ context(BytecodePatchContext)
internal fun Pair.methodOrThrow(parentFingerprint: Pair): MutableMethod =
matchOrThrow(parentFingerprint).method
+context(BytecodePatchContext)
+internal fun Pair.originalMethodOrThrow(): Method =
+ second.originalMethodOrNull ?: throw first.exception
+
+context(BytecodePatchContext)
+internal fun Pair.originalMethodOrThrow(parentFingerprint: Pair): Method =
+ matchOrThrow(parentFingerprint).originalMethod
+
context(BytecodePatchContext)
internal fun Pair.mutableClassOrThrow(): MutableClass =
second.classDefOrNull ?: throw first.exception
diff --git a/patches/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml
index 670fc044e..3bc78a2c0 100644
--- a/patches/src/main/resources/music/settings/host/values/strings.xml
+++ b/patches/src/main/resources/music/settings/host/values/strings.xml
@@ -450,7 +450,16 @@ Tap on the continue button and disable battery optimizations."
Limitations:
• OPUS audio codec may not be supported.
• Seekbar thumbnail may not be present.
-• Watch history does not work with a brand account.
+• Watch history does not work with a brand account."
+
+ Spoof streaming data
+ Spoof the streaming data to prevent playback issues.
+ Show in Stats for nerds
+ Shows the client used to fetch streaming data in Stats for nerds.
+ iOS
+ iOS Music
+ Android TV
+ Android VR
Sanitize sharing links
Removes tracking query parameters from URLs when sharing links.
diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml
index ca18490ac..a07c66a32 100644
--- a/patches/src/main/resources/youtube/settings/host/values/strings.xml
+++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml
@@ -1895,6 +1895,7 @@ Tap on the continue button and disable battery optimizations."
Turning off this setting may cause video playback issues.
Default client
iOS
+ iOS Music
Android TV
Android VR
Spoofing side effects
diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
index 4f54f2a5d..af0ad1d4b 100644
--- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
+++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
@@ -778,10 +778,10 @@