From 7dfd817ba398ba17e944b69f8ca3cd5850002547 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:29:05 +0900 Subject: [PATCH] feat(YouTube Music): Add `Spoof streaming data` patch --- .../music/patches/misc/SpoofClientPatch.java | 39 ++- .../music/patches/misc/client/AppClient.java | 122 --------- .../extension/shared/patches/PatchStatus.java | 6 + .../patches}/client/AppClient.java | 108 ++++---- .../spoof}/SpoofStreamingDataPatch.java | 41 ++- .../patches/spoof}/requests/PlayerRoutes.java | 64 ++--- .../spoof}/requests/StreamingDataRequest.java | 233 +++++++++--------- .../extension/shared/requests/Requester.java | 8 +- .../extension/shared/requests/Route.java | 40 +-- .../shared/settings/BaseSettings.java | 7 + .../patches/video/PlaybackSpeedPatch.java | 2 +- .../requests/PlaylistRequest.java | 13 +- .../extension/youtube/settings/Settings.java | 7 - ...oofStreamingDataSideEffectsPreference.java | 3 +- .../utils/fix/client/SpoofClientPatch.kt | 1 + .../streamingdata/SpoofStreamingDataPatch.kt | 58 +++++ .../patches/music/utils/patch/PatchList.kt | 6 +- .../patches/shared/extension/Constants.kt | 1 + .../BaseSpoofStreamingDataPatch.kt | 215 ++++++++++++++++ .../spoof}/streamingdata/Fingerprints.kt | 82 +++--- .../streamingdata/SpoofStreamingDataPatch.kt | 225 +---------------- .../patches/youtube/utils/patch/PatchList.kt | 2 +- .../util/fingerprint/LegacyFingerprint.kt | 12 + .../music/settings/host/values/strings.xml | 11 +- .../youtube/settings/host/values/strings.xml | 1 + .../youtube/settings/xml/revanced_prefs.xml | 6 +- 26 files changed, 664 insertions(+), 649 deletions(-) delete mode 100644 extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java rename extensions/shared/src/main/java/app/revanced/extension/{youtube/patches/misc => shared/patches}/client/AppClient.java (74%) rename extensions/shared/src/main/java/app/revanced/extension/{youtube/patches/misc => shared/patches/spoof}/SpoofStreamingDataPatch.java (82%) rename extensions/shared/src/main/java/app/revanced/extension/{youtube/patches/misc => shared/patches/spoof}/requests/PlayerRoutes.java (57%) rename extensions/shared/src/main/java/app/revanced/extension/{youtube/patches/misc => shared/patches/spoof}/requests/StreamingDataRequest.java (60%) rename extensions/shared/src/main/java/app/revanced/extension/youtube/patches/{misc => video}/requests/PlaylistRequest.java (94%) create mode 100644 patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt rename patches/src/main/kotlin/app/revanced/patches/{youtube/utils/fix => shared/spoof}/streamingdata/Fingerprints.kt (87%) 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 @@