feat(YouTube Music - Spoof client): Add support for latest versions, Remove 'Spoof streaming data' patch

- 'Spoof client' patch now supports YouTube Music 7.25.53+
- 'Spoof client' patch now spoofs as many innerTube contexts as possible, just like the 'Spoof streaming data' patch
- 'Spoof streaming data' patch is no longer a shared patch
- Nevertheless, 'BlockRequestPatch' is still a shared patch, so each setting is located in a shared path (BaseSettings)
- In YouTube Music 7.16.53 or earlier, 'iOS YouTube Music 6.21' is the only client that does not require 'BlockRequestPatch'
- 'iOS YouTube Music 7.04' supports opus codec when 'Enable OPUS codec' is turned on
This commit is contained in:
inotia00 2025-01-29 19:41:46 +09:00
parent 1151a9a5be
commit 0d2d45a597
27 changed files with 999 additions and 985 deletions

View File

@ -1,118 +0,0 @@
package app.revanced.extension.music.patches.misc.client;
import android.os.Build;
public class AppClient {
// Audio codec is MP4A.
private static final String CLIENT_VERSION_ANDROID_MUSIC_4_27 = "4.27.53";
// Audio codec is OPUS.
private static final String CLIENT_VERSION_ANDROID_MUSIC_5_29 = "5.29.53";
private static final String PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music";
private static final String DEVICE_MODEL_ANDROID_MUSIC = Build.MODEL;
private static final String OS_VERSION_ANDROID_MUSIC = Build.VERSION.RELEASE;
// Audio codec is MP4A.
private static final String CLIENT_VERSION_IOS_MUSIC_6_21 = "6.21";
// Audio codec is OPUS.
private static final String CLIENT_VERSION_IOS_MUSIC_7_04 = "7.04";
private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic";
private static final String DEVICE_MODEL_IOS_MUSIC = "iPhone14,3";
private static final String OS_VERSION_IOS_MUSIC = "15.7.1.19H117";
private static final String USER_AGENT_VERSION_IOS_MUSIC = "15_7_1";
private AppClient() {
}
private static String androidUserAgent(String clientVersion) {
return PACKAGE_NAME_ANDROID_MUSIC +
"/" +
clientVersion +
" (Linux; U; Android " +
OS_VERSION_ANDROID_MUSIC +
"; GB) gzip";
}
private static String iOSUserAgent(String clientVersion) {
return PACKAGE_NAME_IOS_MUSIC +
"/" +
clientVersion +
"(" +
DEVICE_MODEL_IOS_MUSIC +
"; U; CPU iOS " +
USER_AGENT_VERSION_IOS_MUSIC +
" like Mac OS X)";
}
public enum ClientType {
ANDROID_MUSIC_4_27(21,
DEVICE_MODEL_ANDROID_MUSIC,
OS_VERSION_ANDROID_MUSIC,
androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_4_27),
CLIENT_VERSION_ANDROID_MUSIC_4_27
),
ANDROID_MUSIC_5_29(21,
DEVICE_MODEL_ANDROID_MUSIC,
OS_VERSION_ANDROID_MUSIC,
androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_5_29),
CLIENT_VERSION_ANDROID_MUSIC_5_29
),
IOS_MUSIC_6_21(
26,
DEVICE_MODEL_IOS_MUSIC,
OS_VERSION_IOS_MUSIC,
iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_6_21),
CLIENT_VERSION_IOS_MUSIC_6_21
),
IOS_MUSIC_7_04(
26,
DEVICE_MODEL_IOS_MUSIC,
OS_VERSION_IOS_MUSIC,
iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_7_04),
CLIENT_VERSION_IOS_MUSIC_7_04
);
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
public final int id;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String deviceModel;
/**
* Device OS version.
*/
public final String osVersion;
/**
* Player user-agent.
*/
public final String userAgent;
/**
* App version.
*/
public final String clientVersion;
ClientType(int id,
String deviceModel,
String osVersion,
String userAgent,
String clientVersion
) {
this.id = id;
this.deviceModel = deviceModel;
this.clientVersion = clientVersion;
this.osVersion = osVersion;
this.userAgent = userAgent;
}
}
}

View File

@ -8,7 +8,6 @@ import androidx.annotation.NonNull;
import app.revanced.extension.music.patches.general.ChangeStartPagePatch.StartPage; import app.revanced.extension.music.patches.general.ChangeStartPagePatch.StartPage;
import app.revanced.extension.music.patches.misc.AlbumMusicVideoPatch.RedirectType; import app.revanced.extension.music.patches.misc.AlbumMusicVideoPatch.RedirectType;
import app.revanced.extension.music.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.music.patches.utils.PatchStatus; import app.revanced.extension.music.patches.utils.PatchStatus;
import app.revanced.extension.music.sponsorblock.SponsorBlockSettings; import app.revanced.extension.music.sponsorblock.SponsorBlockSettings;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
@ -186,9 +185,6 @@ public class Settings extends BaseSettings {
public static final EnumSetting<RedirectType> DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT, true); public static final EnumSetting<RedirectType> DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT, true);
public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true); public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true);
public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false); public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false);
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
public static final EnumSetting<ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS_MUSIC_6_21, true);
// PreferenceScreen: Return YouTube Dislike // PreferenceScreen: Return YouTube Dislike
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE); public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE);
@ -271,7 +267,6 @@ public class Settings extends BaseSettings {
SETTINGS_IMPORT_EXPORT.key, SETTINGS_IMPORT_EXPORT.key,
SPOOF_APP_VERSION_TARGET.key, SPOOF_APP_VERSION_TARGET.key,
SPOOF_CLIENT_TYPE.key, SPOOF_CLIENT_TYPE.key,
SPOOF_STREAMING_DATA_TYPE.key,
RETURN_YOUTUBE_USERNAME_ABOUT.key, RETURN_YOUTUBE_USERNAME_ABOUT.key,
RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.key, RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.key,
RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.key, RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.key,

View File

@ -20,7 +20,6 @@ import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams;
import static app.revanced.extension.music.utils.RestartUtils.showRestartDialog; import static app.revanced.extension.music.utils.RestartUtils.showRestartDialog;
import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT; import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT;
import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY; import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY;
import static app.revanced.extension.shared.settings.BaseSettings.SPOOF_STREAMING_DATA_TYPE;
import static app.revanced.extension.shared.settings.Setting.getSettingFromPath; import static app.revanced.extension.shared.settings.Setting.getSettingFromPath;
import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray; import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray;
import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.StringRef.str;
@ -165,7 +164,6 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|| settings.equals(DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE) || settings.equals(DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE)
|| settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT) || settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT)
|| settings.equals(SPOOF_CLIENT_TYPE) || settings.equals(SPOOF_CLIENT_TYPE)
|| settings.equals(SPOOF_STREAMING_DATA_TYPE)
) { ) {
ResettableListPreference.showDialog(mActivity, enumSetting, 0); ResettableListPreference.showDialog(mActivity, enumSetting, 0);
} }

View File

@ -6,8 +6,13 @@ public class PatchStatus {
return false; return false;
} }
public static boolean SpoofStreamingDataMusic() { public static boolean SpoofClient() {
// Replace this with true If the Spoof streaming data patch succeeds in YouTube Music // Replace this with true If the Spoof streaming data patch succeeds in YouTube Music.
return false;
}
public static boolean SpoofStreamingData() {
// Replace this with true If the Spoof streaming data patch succeeds in YouTube.
return false; return false;
} }
} }

View File

@ -0,0 +1,189 @@
package app.revanced.extension.shared.patches.client;
import android.os.Build;
import java.util.Locale;
public class MusicAppClient {
// Audio codec is MP4A.
private static final String CLIENT_VERSION_ANDROID_MUSIC_4_27 = "4.27.53";
// Audio codec is OPUS.
private static final String CLIENT_VERSION_ANDROID_MUSIC_5_29 = "5.29.53";
private static final String PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music";
private static final String DEVICE_BRAND_ANDROID_MUSIC = Build.BRAND;
private static final String DEVICE_MAKE_ANDROID_MUSIC = Build.MANUFACTURER;
private static final String DEVICE_MODEL_ANDROID_MUSIC = Build.MODEL;
private static final String BUILD_ID_ANDROID_MUSIC = Build.ID;
private static final String OS_NAME_ANDROID_MUSIC = "Android Automotive";
private static final String OS_VERSION_ANDROID_MUSIC = Build.VERSION.RELEASE;
// Audio codec is MP4A.
private static final String CLIENT_VERSION_IOS_MUSIC_6_21 = "6.21";
private static final String DEVICE_MODEL_IOS_MUSIC_6_21 = "iPhone14,3";
private static final String OS_VERSION_IOS_MUSIC_6_21 = "15.7.1.19H117";
private static final String USER_AGENT_VERSION_IOS_MUSIC_6_21 = "15_7_1";
// Audio codec is OPUS.
private static final String CLIENT_VERSION_IOS_MUSIC_7_04 = "7.04";
private static final String DEVICE_MODEL_IOS_MUSIC_7_04 = "iPhone17,2";
private static final String OS_VERSION_IOS_MUSIC_7_04 = "18.2.1.22C161";
private static final String USER_AGENT_VERSION_IOS_MUSIC_7_04 = "18_2_1";
private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic";
private static final String DEVICE_BRAND_IOS_MUSIC = "Apple";
private static final String DEVICE_MAKE_IOS_MUSIC = "Apple";
private static final String OS_NAME_IOS_MUSIC = "iOS";
private MusicAppClient() {
}
private static String androidUserAgent(String clientVersion) {
return PACKAGE_NAME_ANDROID_MUSIC +
"/" +
clientVersion +
"(Linux; U; Android " +
OS_VERSION_ANDROID_MUSIC +
"; " +
Locale.getDefault() +
"; " +
DEVICE_MODEL_ANDROID_MUSIC +
" Build/" +
BUILD_ID_ANDROID_MUSIC +
") gzip";
}
private static String iOSUserAgent(String clientVersion, String deviceModel, String osVersion) {
return PACKAGE_NAME_IOS_MUSIC +
"/" +
clientVersion +
" (" +
deviceModel +
"; U; CPU iOS " +
osVersion +
" like Mac OS X; " +
Locale.getDefault() +
")";
}
public enum ActionButtonType {
NONE, // No action button (~ 6.14)
YOUTUBE_BUTTON, // Type of action button is YouTubeButton (6.15 ~ 7.16)
LITHO // Type of action button is ComponentHost (7.17 ~)
}
public enum ClientType {
ANDROID_MUSIC_4_27(21,
DEVICE_BRAND_ANDROID_MUSIC,
DEVICE_MAKE_ANDROID_MUSIC,
DEVICE_MODEL_ANDROID_MUSIC,
OS_NAME_ANDROID_MUSIC,
OS_VERSION_ANDROID_MUSIC,
androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_4_27),
CLIENT_VERSION_ANDROID_MUSIC_4_27,
ActionButtonType.NONE
),
ANDROID_MUSIC_5_29(21,
DEVICE_BRAND_ANDROID_MUSIC,
DEVICE_MAKE_ANDROID_MUSIC,
DEVICE_MODEL_ANDROID_MUSIC,
OS_NAME_ANDROID_MUSIC,
OS_VERSION_ANDROID_MUSIC,
androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_5_29),
CLIENT_VERSION_ANDROID_MUSIC_5_29,
ActionButtonType.NONE
),
IOS_MUSIC_6_21(
26,
DEVICE_BRAND_IOS_MUSIC,
DEVICE_MAKE_IOS_MUSIC,
DEVICE_MODEL_IOS_MUSIC_6_21,
OS_NAME_IOS_MUSIC,
OS_VERSION_IOS_MUSIC_6_21,
iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_6_21, DEVICE_MODEL_IOS_MUSIC_6_21, USER_AGENT_VERSION_IOS_MUSIC_6_21),
CLIENT_VERSION_IOS_MUSIC_6_21,
ActionButtonType.YOUTUBE_BUTTON
),
IOS_MUSIC_7_04(
26,
DEVICE_BRAND_IOS_MUSIC,
DEVICE_MAKE_IOS_MUSIC,
DEVICE_MODEL_IOS_MUSIC_7_04,
OS_NAME_IOS_MUSIC,
OS_VERSION_IOS_MUSIC_7_04,
iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_7_04, DEVICE_MODEL_IOS_MUSIC_7_04, USER_AGENT_VERSION_IOS_MUSIC_7_04),
CLIENT_VERSION_IOS_MUSIC_7_04,
ActionButtonType.LITHO
);
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
public final int id;
/**
* Device brand, equivalent to {@link Build#BRAND} (System property: ro.product.vendor.brand)
*/
public final String deviceBrand;
/**
* Device make, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
*/
public final String deviceMake;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String deviceModel;
/**
* Device OS name.
*/
public final String osName;
/**
* Device OS version.
*/
public final String osVersion;
/**
* Player user-agent.
*/
public final String userAgent;
/**
* App version.
*/
public final String clientVersion;
private final ActionButtonType actionButtonType;
ClientType(int id,
String deviceBrand,
String deviceMake,
String deviceModel,
String osName,
String osVersion,
String userAgent,
String clientVersion,
ActionButtonType actionButtonType
) {
this.id = id;
this.deviceBrand = deviceBrand;
this.deviceMake = deviceMake;
this.deviceModel = deviceModel;
this.clientVersion = clientVersion;
this.osName = osName;
this.osVersion = osVersion;
this.userAgent = userAgent;
this.actionButtonType = actionButtonType;
}
public boolean isYouTubeButton() {
return actionButtonType == ActionButtonType.YOUTUBE_BUTTON;
}
}
}

View File

@ -1,7 +1,6 @@
package app.revanced.extension.shared.patches.client package app.revanced.extension.shared.patches.client
import android.os.Build import android.os.Build
import app.revanced.extension.shared.patches.PatchStatus
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
import java.util.Locale import java.util.Locale
@ -9,7 +8,7 @@ import java.util.Locale
/** /**
* Used to fetch streaming data. * Used to fetch streaming data.
*/ */
object AppClient { object YouTubeAppClient {
// IOS // IOS
/** /**
* Video not playable: Paid / Movie / Private / Age-restricted * Video not playable: Paid / Movie / Private / Age-restricted
@ -108,7 +107,6 @@ object AppClient {
*/ */
private const val ANDROID_SDK_VERSION_ANDROID_VR = "32" private const val ANDROID_SDK_VERSION_ANDROID_VR = "32"
private const val BUILD_ID_ANDROID_VR = "SQ3A.220605.009.A1" private const val BUILD_ID_ANDROID_VR = "SQ3A.220605.009.A1"
private const val CHIPSET_ANDROID_VR = "Qualcomm;SXR2230P"
private val USER_AGENT_ANDROID_VR = androidUserAgent( private val USER_AGENT_ANDROID_VR = androidUserAgent(
packageName = PACKAGE_NAME_ANDROID_VR, packageName = PACKAGE_NAME_ANDROID_VR,
@ -137,7 +135,6 @@ object AppClient {
private const val ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34" private const val ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34"
private const val BUILD_ID_ANDROID_UNPLUGGED = "UTT3.240625.001.K5" private const val BUILD_ID_ANDROID_UNPLUGGED = "UTT3.240625.001.K5"
private const val GMS_CORE_VERSION_CODE_ANDROID_UNPLUGGED = "244336107" private const val GMS_CORE_VERSION_CODE_ANDROID_UNPLUGGED = "244336107"
private const val CHIPSET_ANDROID_UNPLUGGED = "Mediatek;MT8696"
private val USER_AGENT_ANDROID_UNPLUGGED = androidUserAgent( private val USER_AGENT_ANDROID_UNPLUGGED = androidUserAgent(
packageName = PACKAGE_NAME_ANDROID_UNPLUGGED, packageName = PACKAGE_NAME_ANDROID_UNPLUGGED,
@ -166,7 +163,6 @@ object AppClient {
private const val ANDROID_SDK_VERSION_ANDROID_CREATOR = "35" private const val ANDROID_SDK_VERSION_ANDROID_CREATOR = "35"
private const val BUILD_ID_ANDROID_CREATOR = "AP3A.241005.015.A2" private const val BUILD_ID_ANDROID_CREATOR = "AP3A.241005.015.A2"
private const val GMS_CORE_VERSION_CODE_ANDROID_CREATOR = "244738035" private const val GMS_CORE_VERSION_CODE_ANDROID_CREATOR = "244738035"
private const val CHIPSET_ANDROID_CREATOR = "Google;Tensor G4"
private val USER_AGENT_ANDROID_CREATOR = androidUserAgent( private val USER_AGENT_ANDROID_CREATOR = androidUserAgent(
packageName = PACKAGE_NAME_ANDROID_CREATOR, packageName = PACKAGE_NAME_ANDROID_CREATOR,
@ -177,39 +173,6 @@ object AppClient {
) )
// ANDROID MUSIC
/**
* Video not playable: All videos that can't be played on YouTube Music
*/
private const val PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music"
/**
* Older client versions don't seem to require poToken.
* It is not the default client yet, as it requires sufficient testing.
*/
private const val CLIENT_VERSION_ANDROID_MUSIC = "4.27.53"
/**
* The device machine id for the Google Pixel 4.
* See [this GitLab](https://dumps.tadiphone.dev/dumps/google/flame) for more information.
*/
private const val DEVICE_MODEL_ANDROID_MUSIC = "Pixel 4"
private const val DEVICE_MAKE_ANDROID_MUSIC = "Google"
private const val OS_VERSION_ANDROID_MUSIC = "11"
private const val ANDROID_SDK_VERSION_ANDROID_MUSIC = "30"
private const val BUILD_ID_ANDROID_MUSIC = "SPP2.210219.008"
private const val GMS_CORE_VERSION_CODE_ANDROID_MUSIC = "244738022"
private const val CHIPSET_ANDROID_MUSIC = "Qualcomm;SM8150"
private val USER_AGENT_ANDROID_MUSIC = androidUserAgent(
packageName = PACKAGE_NAME_ANDROID_MUSIC,
clientVersion = CLIENT_VERSION_ANDROID_MUSIC,
osVersion = OS_VERSION_ANDROID_MUSIC,
deviceModel = DEVICE_MODEL_ANDROID_MUSIC,
buildId = BUILD_ID_ANDROID_MUSIC
)
/** /**
* Same format as Android YouTube User-Agent. * Same format as Android YouTube User-Agent.
* Example: 'com.google.android.youtube/19.46.40(Linux; U; Android 13; in_ID; 21061110AG Build/TP1A.220624.014) gzip' * Example: 'com.google.android.youtube/19.46.40(Linux; U; Android 13; in_ID; 21061110AG Build/TP1A.220624.014) gzip'
@ -240,10 +203,7 @@ object AppClient {
} }
fun availableClientTypes(preferredClient: ClientType): Array<ClientType> { fun availableClientTypes(preferredClient: ClientType): Array<ClientType> {
val availableClientTypes = if (PatchStatus.SpoofStreamingDataMusic()) val availableClientTypes = ClientType.CLIENT_ORDER_TO_USE_YOUTUBE
ClientType.CLIENT_ORDER_TO_USE_YOUTUBE_MUSIC
else
ClientType.CLIENT_ORDER_TO_USE_YOUTUBE
if (ArrayUtils.contains(availableClientTypes, preferredClient)) { if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size) val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
@ -400,19 +360,6 @@ object AppClient {
"iOS Force AVC" "iOS Force AVC"
else else
"iOS" "iOS"
),
ANDROID_MUSIC(
id = 21,
deviceMake = DEVICE_MAKE_ANDROID_MUSIC,
deviceModel = DEVICE_MODEL_ANDROID_MUSIC,
osVersion = OS_VERSION_ANDROID_MUSIC,
userAgent = USER_AGENT_ANDROID_MUSIC,
androidSdkVersion = ANDROID_SDK_VERSION_ANDROID_MUSIC,
clientVersion = CLIENT_VERSION_ANDROID_MUSIC,
gmscoreVersionCode = GMS_CORE_VERSION_CODE_ANDROID_MUSIC,
requireAuth = true,
clientName = "ANDROID_MUSIC",
friendlyName = "Android Music"
); );
companion object { companion object {
@ -424,11 +371,6 @@ object AppClient {
IOS, IOS,
ANDROID_VR_NO_AUTH, ANDROID_VR_NO_AUTH,
) )
internal val CLIENT_ORDER_TO_USE_YOUTUBE_MUSIC: Array<ClientType> = arrayOf(
ANDROID_VR,
ANDROID_MUSIC,
)
} }
} }
} }

View File

@ -4,7 +4,7 @@ package app.revanced.extension.shared.patches.client
* Used to fetch video information. * Used to fetch video information.
*/ */
@Suppress("unused") @Suppress("unused")
object WebClient { object YouTubeWebClient {
/** /**
* This user agent does not require a PoToken in [ClientType.MWEB] * This user agent does not require a PoToken in [ClientType.MWEB]
* https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259 * https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259

View File

@ -0,0 +1,110 @@
package app.revanced.extension.shared.patches.spoof;
import android.net.Uri;
import app.revanced.extension.shared.patches.PatchStatus;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.PackageUtils;
@SuppressWarnings("unused")
public class BlockRequestPatch {
/**
* Used in YouTube.
*/
public static final boolean SPOOF_STREAMING_DATA =
BaseSettings.SPOOF_STREAMING_DATA.get() && PatchStatus.SpoofStreamingData();
/**
* Used in YouTube Music.
*/
public static final boolean SPOOF_CLIENT =
BaseSettings.SPOOF_CLIENT.get() && PatchStatus.SpoofClient();
/**
* In order to load the action bar normally,
* Some versions must block the initplayback request.
*/
private static final boolean IS_7_17_OR_GREATER =
PackageUtils.getAppVersionName().compareTo("7.17.00") >= 0;
private static final boolean IS_YOUTUBE_BUTTON =
BaseSettings.SPOOF_CLIENT_TYPE.get().isYouTubeButton();
private static final boolean BLOCK_REQUEST;
static {
if (SPOOF_STREAMING_DATA) {
BLOCK_REQUEST = true;
} else {
if (!SPOOF_CLIENT) {
BLOCK_REQUEST = false;
} else {
if (!IS_7_17_OR_GREATER && !IS_YOUTUBE_BUTTON) {
// If the current version is lower than 7.16 and the client action button type is not YouTubeButton,
// the initplayback request must be blocked.
BLOCK_REQUEST = true;
} else {
// If the current version is higher than 7.17,
// the initplayback request must always be blocked.
BLOCK_REQUEST = IS_7_17_OR_GREATER;
}
}
}
}
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (BLOCK_REQUEST) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (BLOCK_REQUEST) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by clearing query");
return originalUri.buildUpon().clearQuery().build().toString();
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
}

View File

@ -1,66 +1,98 @@
package app.revanced.extension.music.patches.misc; package app.revanced.extension.shared.patches.spoof;
import app.revanced.extension.music.patches.misc.client.AppClient.ClientType; import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.music.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofClientPatch { public class SpoofClientPatch extends BlockRequestPatch {
private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get();
public static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get();
/** /**
* Injection point. * Injection point.
*/ */
public static int getClientTypeId(int originalClientTypeId) { public static int getClientId(int original) {
if (SPOOF_CLIENT) { if (SPOOF_CLIENT) {
return CLIENT_TYPE.id; return CLIENT_TYPE.id;
} }
return originalClientTypeId; return original;
} }
/** /**
* Injection point. * Injection point.
*/ */
public static String getClientVersion(String originalClientVersion) { public static String getClientVersion(String original) {
if (SPOOF_CLIENT) { if (SPOOF_CLIENT) {
return CLIENT_TYPE.clientVersion; return CLIENT_TYPE.clientVersion;
} }
return originalClientVersion; return original;
} }
/** /**
* Injection point. * Injection point.
*/ */
public static String getClientModel(String originalClientModel) { public static String getDeviceBrand(String original) {
if (SPOOF_CLIENT) {
return CLIENT_TYPE.deviceBrand;
}
return original;
}
/**
* Injection point.
*/
public static String getDeviceMake(String original) {
if (SPOOF_CLIENT) {
return CLIENT_TYPE.deviceMake;
}
return original;
}
/**
* Injection point.
*/
public static String getDeviceModel(String original) {
if (SPOOF_CLIENT) { if (SPOOF_CLIENT) {
return CLIENT_TYPE.deviceModel; return CLIENT_TYPE.deviceModel;
} }
return originalClientModel; return original;
} }
/** /**
* Injection point. * Injection point.
*/ */
public static String getOsVersion(String originalOsVersion) { public static String getOsName(String original) {
if (SPOOF_CLIENT) {
return CLIENT_TYPE.osName;
}
return original;
}
/**
* Injection point.
*/
public static String getOsVersion(String original) {
if (SPOOF_CLIENT) { if (SPOOF_CLIENT) {
return CLIENT_TYPE.osVersion; return CLIENT_TYPE.osVersion;
} }
return originalOsVersion; return original;
} }
/** /**
* Injection point. * Injection point.
*/ */
public static String getUserAgent(String originalUserAgent) { public static String getUserAgent(String original) {
if (SPOOF_CLIENT) { if (SPOOF_CLIENT) {
return CLIENT_TYPE.userAgent; return CLIENT_TYPE.userAgent;
} }
return originalUserAgent; return original;
} }
/** /**

View File

@ -1,11 +1,8 @@
package app.revanced.extension.shared.patches.spoof; package app.revanced.extension.shared.patches.spoof;
import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataMusic;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -13,7 +10,7 @@ import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import app.revanced.extension.shared.patches.client.AppClient.ClientType; import app.revanced.extension.shared.patches.client.YouTubeAppClient.ClientType;
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest; import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
@ -21,10 +18,7 @@ import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofStreamingDataPatch { public class SpoofStreamingDataPatch extends BlockRequestPatch {
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_STREAMING_DATA.get();
private static final boolean SPOOF_STREAMING_DATA_YOUTUBE = SPOOF_STREAMING_DATA && !SpoofStreamingDataMusic();
private static final boolean SPOOF_STREAMING_DATA_MUSIC = SPOOF_STREAMING_DATA && SpoofStreamingDataMusic();
private static final String PO_TOKEN = private static final String PO_TOKEN =
BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get(); BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get();
private static final String VISITOR_DATA = private static final String VISITOR_DATA =
@ -50,58 +44,6 @@ public class SpoofStreamingDataPatch {
} }
}); });
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_STREAMING_DATA) {
// An exception may be thrown when the /get_watch request is blocked when connected to Wi-Fi in YouTube Music.
if (SPOOF_STREAMING_DATA_YOUTUBE || Utils.getNetworkType() == Utils.NetworkType.MOBILE) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_STREAMING_DATA) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by clearing query");
return originalUri.buildUpon().clearQuery().build().toString();
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
/** /**
* Injection point. * Injection point.
*/ */
@ -120,73 +62,20 @@ public class SpoofStreamingDataPatch {
return false; return false;
} }
private static volatile String auth = ""; /**
private static volatile Map<String, String> requestHeader; * Parameters causing playback issues.
*/
private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String[] PATH_NO_VIDEO_ID = {
"ad_break", // This request fetches a list of times when ads can be displayed.
private static final String[] REQUEST_HEADER_KEYS = { "get_drm_license", // Waiting for a paid video to start.
AUTHORIZATION_HEADER, "heartbeat", // This request determines whether to pause playback when the user is AFK.
"X-GOOG-API-FORMAT-VERSION", "refresh", // Waiting for a livestream to start.
"X-Goog-Visitor-Id"
}; };
/**
* If the /get_watch request is not blocked,
* fetchRequest will not be invoked at the point where the video starts.
* <p>
* An additional method is used to invoke fetchRequest in YouTube Music:
* 1. Save the requestHeader in a field.
* 2. Invoke fetchRequest with the videoId used in PlaybackStartDescriptor.
* <p>
*
* @param requestHeaders Save the request Headers used for login to a field.
* Only used in YouTube Music where login is required.
*/
private static void setRequestHeaders(Map<String, String> requestHeaders) {
if (SPOOF_STREAMING_DATA_MUSIC) {
try {
// Save requestHeaders whenever an account is switched.
String authorization = requestHeaders.get(AUTHORIZATION_HEADER);
if (authorization == null || auth.equals(authorization)) {
return;
}
for (String key : REQUEST_HEADER_KEYS) {
if (requestHeaders.get(key) == null) {
return;
}
}
auth = authorization;
requestHeader = requestHeaders;
} catch (Exception ex) {
Logger.printException(() -> "setRequestHeaders failure", ex);
}
}
}
/**
* Injection point.
*/
public static void fetchStreams(@NonNull String videoId) {
if (SPOOF_STREAMING_DATA_MUSIC) {
try {
if (requestHeader != null) {
StreamingDataRequest.fetchRequest(videoId, requestHeader, VISITOR_DATA, PO_TOKEN);
} else {
Logger.printDebug(() -> "Ignoring request with no header.");
}
} catch (Exception ex) {
Logger.printException(() -> "fetchStreams failure", ex);
}
}
}
/** /**
* Injection point. * Injection point.
*/ */
public static void fetchStreams(String url, Map<String, String> requestHeaders) { public static void fetchStreams(String url, Map<String, String> requestHeaders) {
setRequestHeaders(requestHeaders);
if (SPOOF_STREAMING_DATA) { if (SPOOF_STREAMING_DATA) {
try { try {
Uri uri = Uri.parse(url); Uri uri = Uri.parse(url);
@ -195,11 +84,7 @@ public class SpoofStreamingDataPatch {
return; return;
} }
// 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start. if (Utils.containsAny(path, PATH_NO_VIDEO_ID)) {
// 'heartbeat' has no video id and appears to be only after playback has started.
// 'refresh' has no video id and appears to happen when waiting for a livestream to start.
// 'ad_break' has no video id.
if (path.contains("get_drm_license") || path.contains("heartbeat") || path.contains("refresh") || path.contains("ad_break")) {
Logger.printDebug(() -> "Ignoring path: " + path); Logger.printDebug(() -> "Ignoring path: " + path);
return; return;
} }
@ -262,7 +147,7 @@ public class SpoofStreamingDataPatch {
* Called after {@link #getStreamingData(String)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static void setApproxDurationMs(String videoId, long approxDurationMs) { public static void setApproxDurationMs(String videoId, long approxDurationMs) {
if (SPOOF_STREAMING_DATA_YOUTUBE && approxDurationMs != Long.MAX_VALUE) { if (SPOOF_STREAMING_DATA && approxDurationMs != Long.MAX_VALUE) {
approxDurationMsMap.put(videoId, approxDurationMs); approxDurationMsMap.put(videoId, approxDurationMs);
Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs); Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs);
} }
@ -284,7 +169,7 @@ public class SpoofStreamingDataPatch {
* Called after {@link #getStreamingData(String)}. * Called after {@link #getStreamingData(String)}.
*/ */
public static long getApproxDurationMs(String videoId) { public static long getApproxDurationMs(String videoId) {
if (SPOOF_STREAMING_DATA_YOUTUBE && videoId != null) { if (SPOOF_STREAMING_DATA && videoId != null) {
final Long approxDurationMs = approxDurationMsMap.get(videoId); final Long approxDurationMs = approxDurationMsMap.get(videoId);
if (approxDurationMs != null) { if (approxDurationMs != null) {
Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId); Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId);

View File

@ -1,7 +1,7 @@
package app.revanced.extension.shared.patches.spoof.requests package app.revanced.extension.shared.patches.spoof.requests
import app.revanced.extension.shared.patches.client.AppClient import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.WebClient import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.requests.Route import app.revanced.extension.shared.requests.Route
import app.revanced.extension.shared.requests.Route.CompiledRoute import app.revanced.extension.shared.requests.Route.CompiledRoute
@ -69,7 +69,7 @@ object PlayerRoutes {
@JvmStatic @JvmStatic
fun createApplicationRequestBody( fun createApplicationRequestBody(
clientType: AppClient.ClientType, clientType: YouTubeAppClient.ClientType,
videoId: String, videoId: String,
playlistId: String? = null, playlistId: String? = null,
botGuardPoToken: String = "", botGuardPoToken: String = "",
@ -130,7 +130,7 @@ object PlayerRoutes {
@JvmStatic @JvmStatic
fun createWebInnertubeBody( fun createWebInnertubeBody(
clientType: WebClient.ClientType, clientType: YouTubeWebClient.ClientType,
videoId: String videoId: String
): ByteArray { ): ByteArray {
val innerTubeBody = JSONObject() val innerTubeBody = JSONObject()
@ -161,7 +161,7 @@ object PlayerRoutes {
@JvmStatic @JvmStatic
fun getPlayerResponseConnectionFromRoute( fun getPlayerResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
clientType: AppClient.ClientType clientType: YouTubeAppClient.ClientType
): HttpURLConnection { ): HttpURLConnection {
return getPlayerResponseConnectionFromRoute( return getPlayerResponseConnectionFromRoute(
route, route,
@ -174,7 +174,7 @@ object PlayerRoutes {
@JvmStatic @JvmStatic
fun getPlayerResponseConnectionFromRoute( fun getPlayerResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
clientType: WebClient.ClientType clientType: YouTubeWebClient.ClientType
): HttpURLConnection { ): HttpURLConnection {
return getPlayerResponseConnectionFromRoute( return getPlayerResponseConnectionFromRoute(
route, route,

View File

@ -1,8 +1,7 @@
package app.revanced.extension.shared.patches.spoof.requests package app.revanced.extension.shared.patches.spoof.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.AppClient import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.AppClient.availableClientTypes
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute
@ -93,16 +92,16 @@ class StreamingDataRequest private constructor(
"X-GOOG-API-FORMAT-VERSION", "X-GOOG-API-FORMAT-VERSION",
VISITOR_ID_HEADER VISITOR_ID_HEADER
) )
private val SPOOF_STREAMING_DATA_TYPE: AppClient.ClientType = private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get() BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
private val CLIENT_ORDER_TO_USE: Array<AppClient.ClientType> = private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
availableClientTypes(SPOOF_STREAMING_DATA_TYPE) YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean = private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
SPOOF_STREAMING_DATA_TYPE == AppClient.ClientType.ANDROID_VR_NO_AUTH SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
private var lastSpoofedClientType: AppClient.ClientType? = null private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null
/** /**
@ -163,7 +162,7 @@ class StreamingDataRequest private constructor(
} }
private fun send( private fun send(
clientType: AppClient.ClientType, clientType: YouTubeAppClient.ClientType,
videoId: String, videoId: String,
playerHeaders: Map<String, String>, playerHeaders: Map<String, String>,
visitorId: String, visitorId: String,
@ -279,7 +278,7 @@ class StreamingDataRequest private constructor(
} else { } else {
BufferedInputStream(connection.inputStream).use { inputStream -> BufferedInputStream(connection.inputStream).use { inputStream ->
ByteArrayOutputStream().use { stream -> ByteArrayOutputStream().use { stream ->
val buffer = ByteArray(4096) val buffer = ByteArray(2048)
var bytesRead: Int var bytesRead: Int
while ((inputStream.read(buffer) while ((inputStream.read(buffer)
.also { bytesRead = it }) >= 0 .also { bytesRead = it }) >= 0

View File

@ -5,7 +5,8 @@ import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean; import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat; import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.client.AppClient.ClientType; import app.revanced.extension.shared.patches.client.MusicAppClient;
import app.revanced.extension.shared.patches.client.YouTubeAppClient;
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability; import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability;
/** /**
@ -25,6 +26,28 @@ public class BaseSettings {
public static final EnumSetting<AppLanguage> REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true); public static final EnumSetting<AppLanguage> REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true);
/**
* These settings are used by YouTube Music.
* Some patches are in a shared path, so they are declared here.
*/
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true);
public static final EnumSetting<MusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true);
/**
* These settings are used by YouTube.
* Some patches are in a shared path, so they are declared here.
*/
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<AppLanguage> SPOOF_STREAMING_DATA_LANGUAGE = new EnumSetting<>("revanced_spoof_streaming_data_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true,
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
// Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true);
public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
/** /**
* These settings are used by YouTube and YouTube Music. * These settings are used by YouTube and YouTube Music.
*/ */
@ -39,17 +62,6 @@ public class BaseSettings {
public static final EnumSetting<DisplayFormat> RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = new EnumSetting<>("revanced_return_youtube_username_display_format", DisplayFormat.USERNAME_ONLY, true); public static final EnumSetting<DisplayFormat> 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 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<AppLanguage> SPOOF_STREAMING_DATA_LANGUAGE = new EnumSetting<>("revanced_spoof_streaming_data_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true,
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
// Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true);
public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
/** /**
* @noinspection DeprecatedIsStillUsed * @noinspection DeprecatedIsStillUsed
*/ */

View File

@ -2,7 +2,7 @@ package app.revanced.extension.youtube.patches.general.requests
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.WebClient import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
@ -81,7 +81,7 @@ class VideoDetailsRequest private constructor(
private fun sendRequest(videoId: String): JSONObject? { private fun sendRequest(videoId: String): JSONObject? {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val clientType = WebClient.ClientType.MWEB val clientType = YouTubeWebClient.ClientType.MWEB
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }

View File

@ -2,8 +2,8 @@ package app.revanced.extension.youtube.patches.video.requests
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.AppClient import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.WebClient import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
@ -119,7 +119,7 @@ class MusicRequest private constructor(
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val clientType = AppClient.ClientType.ANDROID_VR val clientType = YouTubeAppClient.ClientType.ANDROID_VR
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
@ -163,7 +163,7 @@ class MusicRequest private constructor(
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val clientType = WebClient.ClientType.MWEB val clientType = YouTubeWebClient.ClientType.MWEB
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }

View File

@ -2,13 +2,9 @@ package app.revanced.patches.music.utils.fix.client
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val createPlayerRequestBodyFingerprint = legacyFingerprint( internal val createPlayerRequestBodyFingerprint = legacyFingerprint(
name = "createPlayerRequestBodyFingerprint", name = "createPlayerRequestBodyFingerprint",
@ -22,26 +18,6 @@ internal val createPlayerRequestBodyFingerprint = legacyFingerprint(
strings = listOf("ms"), strings = listOf("ms"),
) )
internal val createPlayerRequestBodyWithVersionReleaseFingerprint = legacyFingerprint(
name = "createPlayerRequestBodyWithVersionReleaseFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("L"),
strings = listOf("Google Inc."),
customFingerprint = { method, _ ->
indexOfBuildInstruction(method) >= 0
},
)
fun indexOfBuildInstruction(method: Method) =
method.indexOfFirstInstruction {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.name == "build" &&
reference.parameterTypes.isEmpty() &&
reference.returnType.startsWith("L")
}
internal val setPlayerRequestClientTypeFingerprint = legacyFingerprint( internal val setPlayerRequestClientTypeFingerprint = legacyFingerprint(
name = "setPlayerRequestClientTypeFingerprint", name = "setPlayerRequestClientTypeFingerprint",
opcodes = listOf( opcodes = listOf(

View File

@ -3,24 +3,28 @@ package app.revanced.patches.music.utils.fix.client
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.music.utils.compatibility.Constants import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.extension.Constants.MISC_PATH
import app.revanced.patches.music.utils.patch.PatchList.SPOOF_CLIENT import app.revanced.patches.music.utils.patch.PatchList.SPOOF_CLIENT
import app.revanced.patches.music.utils.playbackSpeedBottomSheetFingerprint import app.revanced.patches.music.utils.playbackSpeedBottomSheetFingerprint
import app.revanced.patches.music.utils.playservice.is_7_25_or_greater
import app.revanced.patches.music.utils.playservice.versionCheckPatch
import app.revanced.patches.music.utils.settings.CategoryType import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.indexOfBrandInstruction
import app.revanced.patches.shared.indexOfManufacturerInstruction
import app.revanced.patches.shared.indexOfModelInstruction import app.revanced.patches.shared.indexOfModelInstruction
import app.revanced.util.Utils.printWarn import app.revanced.patches.shared.indexOfReleaseInstruction
import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow import app.revanced.util.fingerprint.mutableClassOrThrow
@ -32,109 +36,118 @@ import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction 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.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.Reference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
private const val EXTENSION_CLASS_DESCRIPTOR = private const val EXTENSION_CLASS_DESCRIPTOR =
"$MISC_PATH/SpoofClientPatch;" "$SPOOF_PATH/SpoofClientPatch;"
private const val CLIENT_INFO_CLASS_DESCRIPTOR = private const val CLIENT_INFO_CLASS_DESCRIPTOR =
"Lcom/google/protos/youtube/api/innertube/InnertubeContext\$ClientInfo;" "Lcom/google/protos/youtube/api/innertube/InnertubeContext\$ClientInfo;"
@Suppress("unused") @Suppress("unused")
val spoofClientPatch = bytecodePatch( val spoofClientPatch = bytecodePatch(
SPOOF_CLIENT.title, SPOOF_CLIENT.title,
SPOOF_CLIENT.summary, SPOOF_CLIENT.summary
false,
) { ) {
compatibleWith( compatibleWith(COMPATIBLE_PACKAGE)
Constants.YOUTUBE_MUSIC_PACKAGE_NAME(
"6.20.51",
"6.29.59",
"6.42.55",
"6.51.53",
"7.16.53",
),
)
dependsOn( dependsOn(
settingsPatch, settingsPatch,
versionCheckPatch, blockRequestPatch,
) )
execute { execute {
if (is_7_25_or_greater) { lateinit var clientInfoReference: Reference
printWarn("\"${SPOOF_CLIENT.title}\" is not supported in this version. Use YouTube Music 7.24.51 or earlier.") lateinit var clientIdReference: Reference
return@execute lateinit var clientVersionReference: Reference
} lateinit var deviceBrandReference: Reference
lateinit var deviceMakeReference: Reference
lateinit var deviceModelReference: Reference
lateinit var osNameReference: Reference
lateinit var osVersionReference: Reference
fun MutableMethod.getFieldReference(index: Int) =
getInstruction<ReferenceInstruction>(index).reference
// region Get field references to be used below. // region Get field references to be used below.
val (clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField) = setPlayerRequestClientTypeFingerprint.matchOrThrow().let {
setPlayerRequestClientTypeFingerprint.matchOrThrow().let { result -> it.method.apply {
with(result.method) { val clientInfoIndex = indexOfFirstInstructionOrThrow {
// Field in the player request object that holds the client info object.
val clientInfoField = instructions.find { instruction ->
// requestMessage.clientInfo = clientInfoBuilder.build();
instruction.opcode == Opcode.IPUT_OBJECT &&
instruction.getReference<FieldReference>()?.type == CLIENT_INFO_CLASS_DESCRIPTOR
}?.getReference<FieldReference>()
?: throw PatchException("Could not find clientInfoField")
// Client info object's client type field.
val clientInfoClientTypeField =
getInstruction(result.patternMatch!!.endIndex)
.getReference<FieldReference>()
?: throw PatchException("Could not find clientInfoClientTypeField")
val clientInfoVersionIndex = result.stringMatches!!.first().index
val clientInfoVersionRegister =
getInstruction<OneRegisterInstruction>(clientInfoVersionIndex).registerA
val clientInfoClientVersionFieldIndex =
indexOfFirstInstructionOrThrow(clientInfoVersionIndex) {
opcode == Opcode.IPUT_OBJECT &&
(this as TwoRegisterInstruction).registerA == clientInfoVersionRegister
}
// Client info object's client version field.
val clientInfoClientVersionField =
getInstruction(clientInfoClientVersionFieldIndex)
.getReference<FieldReference>()
?: throw PatchException("Could not find clientInfoClientVersionField")
Triple(clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField)
}
}
val clientInfoClientModelField =
with(createPlayerRequestBodyWithModelFingerprint.methodOrThrow()) {
// The next IPUT_OBJECT instruction after getting the client model is setting the client model field.
val clientInfoClientModelIndex =
indexOfFirstInstructionOrThrow(indexOfModelInstruction(this)) {
val reference = getReference<FieldReference>()
opcode == Opcode.IPUT_OBJECT &&
reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR &&
reference.type == "Ljava/lang/String;"
}
getInstruction<ReferenceInstruction>(clientInfoClientModelIndex).reference
}
val clientInfoOsVersionField =
with(createPlayerRequestBodyWithVersionReleaseFingerprint.methodOrThrow()) {
val buildIndex = indexOfBuildInstruction(this)
val clientInfoOsVersionIndex = indexOfFirstInstructionOrThrow(buildIndex - 5) {
val reference = getReference<FieldReference>()
opcode == Opcode.IPUT_OBJECT && opcode == Opcode.IPUT_OBJECT &&
reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && getReference<FieldReference>()?.type == CLIENT_INFO_CLASS_DESCRIPTOR
reference.type == "Ljava/lang/String;"
} }
getInstruction<ReferenceInstruction>(clientInfoOsVersionIndex).reference val clientIdIndex = it.patternMatch!!.endIndex
val dummyClientVersionIndex = it.stringMatches!!.first().index
val dummyClientVersionRegister =
getInstruction<OneRegisterInstruction>(dummyClientVersionIndex).registerA
val clientVersionIndex =
indexOfFirstInstructionOrThrow(dummyClientVersionIndex) {
opcode == Opcode.IPUT_OBJECT &&
(this as TwoRegisterInstruction).registerA == dummyClientVersionRegister
}
clientInfoReference =
getFieldReference(clientInfoIndex)
clientIdReference =
getFieldReference(clientIdIndex)
clientVersionReference =
getFieldReference(clientVersionIndex)
} }
}
fun MutableMethod.getClientInfoIndex(
startIndex: Int,
reversed: Boolean = false
): Int {
val filter: Instruction.() -> Boolean = {
val reference = getReference<FieldReference>()
opcode == Opcode.IPUT_OBJECT &&
reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR &&
reference.type == "Ljava/lang/String;"
}
return if (reversed) {
indexOfFirstInstructionReversedOrThrow(startIndex, filter)
} else {
indexOfFirstInstructionOrThrow(startIndex, filter)
}
}
createPlayerRequestBodyWithModelFingerprint.methodOrThrow().apply {
val buildManufacturerIndex =
indexOfManufacturerInstruction(this)
val deviceBrandIndex =
getClientInfoIndex(indexOfBrandInstruction(this))
val deviceMakeIndex =
getClientInfoIndex(buildManufacturerIndex)
val deviceModelIndex =
getClientInfoIndex(indexOfModelInstruction(this))
val chipSetIndex =
getClientInfoIndex(buildManufacturerIndex, true)
val osNameIndex =
getClientInfoIndex(chipSetIndex - 1, true)
val osVersionIndex =
getClientInfoIndex(indexOfReleaseInstruction(this))
deviceBrandReference =
getFieldReference(deviceBrandIndex)
deviceMakeReference =
getFieldReference(deviceMakeIndex)
deviceModelReference =
getFieldReference(deviceModelIndex)
osNameReference =
getFieldReference(osNameIndex)
osVersionReference =
getFieldReference(osVersionIndex)
}
// endregion // endregion
@ -181,31 +194,49 @@ val spoofClientPatch = bytecodePatch(
move-result v0 move-result v0
if-eqz v0, :disabled if-eqz v0, :disabled
iget-object v0, p0, $clientInfoField iget-object v0, p0, $clientInfoReference
# Set client type to the spoofed value. # Set client id.
iget v1, v0, $clientInfoClientTypeField iget v1, v0, $clientIdReference
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientTypeId(I)I invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientId(I)I
move-result v1 move-result v1
iput v1, v0, $clientInfoClientTypeField iput v1, v0, $clientIdReference
# Set client model to the spoofed value.
iget-object v1, v0, $clientInfoClientModelField
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientModel(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $clientInfoClientModelField
# Set client version to the spoofed value. # Set client version.
iget-object v1, v0, $clientInfoClientVersionField iget-object v1, v0, $clientVersionReference
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientVersion(Ljava/lang/String;)Ljava/lang/String; invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientVersion(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1 move-result-object v1
iput-object v1, v0, $clientInfoClientVersionField iput-object v1, v0, $clientVersionReference
# Set client os version to the spoofed value. # Set device brand.
iget-object v1, v0, $clientInfoOsVersionField iget-object v1, v0, $deviceBrandReference
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getDeviceBrand(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $deviceBrandReference
# Set device make.
iget-object v1, v0, $deviceMakeReference
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getDeviceMake(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $deviceMakeReference
# Set device model.
iget-object v1, v0, $deviceModelReference
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getDeviceModel(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $deviceModelReference
# Set os name.
iget-object v1, v0, $osNameReference
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getOsName(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $osNameReference
# Set os version.
iget-object v1, v0, $osVersionReference
invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getOsVersion(Ljava/lang/String;)Ljava/lang/String; invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getOsVersion(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1 move-result-object v1
iput-object v1, v0, $clientInfoOsVersionField iput-object v1, v0, $osVersionReference
:disabled :disabled
return-void return-void
@ -273,10 +304,17 @@ val spoofClientPatch = bytecodePatch(
// endregion // endregion
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
name == "SpoofClient"
}.replaceInstruction(
0,
"const/4 v0, 0x1"
)
addSwitchPreference( addSwitchPreference(
CategoryType.MISC, CategoryType.MISC,
"revanced_spoof_client", "revanced_spoof_client",
"false" "true"
) )
addPreferenceWithIntent( addPreferenceWithIntent(
CategoryType.MISC, CategoryType.MISC,

View File

@ -1,67 +0,0 @@
package app.revanced.patches.music.utils.fix.streamingdata
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.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.music.video.playerresponse.hookPlayerResponse
import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
import app.revanced.util.findMethodOrThrow
const val EXTENSION_CLASS_DESCRIPTOR =
"$SPOOF_PATH/SpoofStreamingDataPatch;"
@Suppress("unused")
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
{
compatibleWith(COMPATIBLE_PACKAGE)
dependsOn(
baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME),
settingsPatch,
playerResponseMethodHookPatch,
)
},
{
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
name == "SpoofStreamingDataMusic"
}.replaceInstruction(
0,
"const/4 v0, 0x1"
)
hookPlayerResponse(
"$EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;)V",
true
)
addSwitchPreference(
CategoryType.MISC,
"revanced_spoof_streaming_data",
"true"
)
addPreferenceWithIntent(
CategoryType.MISC,
"revanced_spoof_streaming_data_type",
"revanced_spoof_streaming_data"
)
addSwitchPreference(
CategoryType.MISC,
"revanced_spoof_streaming_data_stats_for_nerds",
"true",
"revanced_spoof_streaming_data"
)
updatePatchStatus(SPOOF_STREAMING_DATA)
}
)

View File

@ -153,10 +153,6 @@ internal enum class PatchList(
"Spoof client", "Spoof client",
"Adds options to spoof the client to allow 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(
"Translations for YouTube Music", "Translations for YouTube Music",
"Add translations or remove string resources." "Add translations or remove string resources."

View File

@ -18,11 +18,19 @@ internal val createPlayerRequestBodyWithModelFingerprint = legacyFingerprint(
parameters = emptyList(), parameters = emptyList(),
opcodes = listOf(Opcode.OR_INT_LIT16), opcodes = listOf(Opcode.OR_INT_LIT16),
customFingerprint = { method, _ -> customFingerprint = { method, _ ->
indexOfModelInstruction(method) >= 0 && indexOfBrandInstruction(method) >= 0 &&
indexOfManufacturerInstruction(method) >= 0 &&
indexOfModelInstruction(method) >= 0 &&
indexOfReleaseInstruction(method) >= 0 indexOfReleaseInstruction(method) >= 0
} }
) )
fun indexOfBrandInstruction(method: Method) =
method.indexOfFieldReference("Landroid/os/Build;->BRAND:Ljava/lang/String;")
fun indexOfManufacturerInstruction(method: Method) =
method.indexOfFieldReference("Landroid/os/Build;->MANUFACTURER:Ljava/lang/String;")
fun indexOfModelInstruction(method: Method) = fun indexOfModelInstruction(method: Method) =
method.indexOfFieldReference("Landroid/os/Build;->MODEL:Ljava/lang/String;") method.indexOfFieldReference("Landroid/os/Build;->MODEL:Ljava/lang/String;")

View File

@ -0,0 +1,57 @@
package app.revanced.patches.shared.spoof.blockrequest
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
const val EXTENSION_CLASS_DESCRIPTOR =
"$SPOOF_PATH/BlockRequestPatch;"
val blockRequestPatch = bytecodePatch(
description = "blockRequestPatch"
) {
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<OneRegisterInstruction>(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<FiveRegisterInstruction>(invokeToStringIndex).registerC
addInstructions(
invokeToStringIndex,
"""
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
move-result-object v$uriRegister
""",
)
}
// endregion
}
}

View File

@ -0,0 +1,40 @@
package app.revanced.patches.shared.spoof.blockrequest
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val buildInitPlaybackRequestFingerprint = legacyFingerprint(
name = "buildInitPlaybackRequestFingerprint",
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
opcodes = listOf(
Opcode.MOVE_RESULT_OBJECT,
Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with.
),
strings = listOf(
"Content-Type",
"Range",
),
)
internal val buildPlayerRequestURIFingerprint = legacyFingerprint(
name = "buildPlayerRequestURIFingerprint",
returnType = "Ljava/lang/String;",
strings = listOf(
"key",
"asig",
),
customFingerprint = { method, _ ->
indexOfToStringInstruction(method) >= 0
},
)
internal fun indexOfToStringInstruction(method: Method) =
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;"
}

View File

@ -1,370 +0,0 @@
package app.revanced.patches.shared.spoof.streamingdata
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
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.extensions.InstructionExtensions.removeInstruction
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.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.fingerprint.definingClassOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
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.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
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<OneRegisterInstruction>(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<FiveRegisterInstruction>(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<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
val entrySetIndex = indexOfEntrySetInstruction(this)
val mapRegister = if (entrySetIndex < 0)
urlRegister + 1
else
getInstruction<FiveRegisterInstruction>(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.
val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let {
with(it.method) {
getInstruction<ReferenceInstruction>(it.patternMatch!!.startIndex).reference
}
}
val streamingDataFormatsReference = with(
videoStreamingDataConstructorFingerprint.methodOrThrow(
videoStreamingDataToStringFingerprint
)
) {
val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this)
val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex)
val longMaxValueRegister =
getInstruction<OneRegisterInstruction>(longMaxValueIndex).registerA
val videoIdIndex =
indexOfFirstInstructionOrThrow(longMaxValueIndex) {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.type == "Ljava/lang/String;" &&
reference.definingClass == definingClass
}
val definingClassRegister =
getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB
val videoIdReference =
getInstruction<ReferenceInstruction>(videoIdIndex).reference
addInstructions(
longMaxValueIndex + 1, """
# Get video id.
iget-object v$longMaxValueRegister, v$definingClassRegister, $videoIdReference
# Override approxDurationMs.
invoke-static { v$longMaxValueRegister }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMs(Ljava/lang/String;)J
move-result-wide v$longMaxValueRegister
"""
)
removeInstruction(longMaxValueIndex)
getInstruction<ReferenceInstruction>(getFormatsFieldIndex).reference
}
createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint)
.let { result ->
result.method.apply {
val setStreamDataMethodName = "patch_setStreamingData"
val calcApproxDurationMsMethodName = "patch_calcApproxDurationMs"
val resultClassDef = result.classDef
val resultMethodType = resultClassDef.type
val setStreamingDataIndex = result.patternMatch!!.startIndex
val setStreamingDataField =
getInstruction(setStreamingDataIndex).getReference<FieldReference>()
.toString()
val playerProtoClass =
getInstruction(setStreamingDataIndex + 1).getReference<FieldReference>()!!.definingClass
val protobufClass =
protobufClassParseByteBufferFingerprint.definingClassOrThrow()
val getStreamingDataField = instructions.find { instruction ->
instruction.opcode == Opcode.IGET_OBJECT &&
instruction.getReference<FieldReference>()?.definingClass == playerProtoClass
}?.getReference<FieldReference>()
?: throw PatchException("Could not find getStreamingDataField")
val videoDetailsIndex = result.patternMatch!!.endIndex
val videoDetailsRegister =
getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA
val videoDetailsClass =
getInstruction(videoDetailsIndex).getReference<FieldReference>()!!.type
addInstruction(
videoDetailsIndex + 1,
"invoke-direct { p0, v$videoDetailsRegister }, " +
"$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V",
)
result.classDef.methods.add(
ImmutableMethod(
resultMethodType,
setStreamDataMethodName,
listOf(
ImmutableMethodParameter(
videoDetailsClass,
annotations,
"videoDetails"
)
),
"V",
AccessFlags.PRIVATE.value or AccessFlags.FINAL.value,
annotations,
null,
MutableMethodImplementation(9),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
move-result v0
if-eqz v0, :disabled
# Get video id.
iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String;
if-eqz v2, :disabled
# Get streaming data.
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
move-result-object v3
if-eqz v3, :disabled
# Parse streaming data.
sget-object v4, $playerProtoClass->a:$playerProtoClass
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
move-result-object v5
check-cast v5, $playerProtoClass
iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled
# Caculate approxDurationMs.
invoke-direct { p0, v2 }, $resultMethodType->$calcApproxDurationMsMethodName(Ljava/lang/String;)V
# Set spoofed streaming data.
iput-object v6, p0, $setStreamingDataField
:disabled
return-void
""",
)
},
)
resultClassDef.methods.add(
ImmutableMethod(
resultMethodType,
calcApproxDurationMsMethodName,
listOf(
ImmutableMethodParameter(
"Ljava/lang/String;",
annotations,
"videoId"
)
),
"V",
AccessFlags.PRIVATE.value or AccessFlags.FINAL.value,
annotations,
null,
MutableMethodImplementation(12),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
# Get video format list.
iget-object v0, p0, $setStreamingDataField
iget-object v0, v0, $streamingDataFormatsReference
invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;
move-result-object v0
# Initialize approxDurationMs field.
const-wide v1, 0x7fffffffffffffffL
:loop
# Loop over all video formats to get the approxDurationMs
invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z
move-result v3
const-wide/16 v4, 0x0
if-eqz v3, :exit
invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v3
check-cast v3, ${(approxDurationMsReference as FieldReference).definingClass}
# Get approxDurationMs from format
iget-wide v6, v3, $approxDurationMsReference
# Compare with zero to make sure approxDurationMs is not negative
cmp-long v8, v6, v4
if-lez v8, :loop
# Only use the min value of approxDurationMs
invoke-static {v1, v2, v6, v7}, Ljava/lang/Math;->min(JJ)J
move-result-wide v1
goto :loop
:exit
# Save approxDurationMs to integrations
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;J)V
return-void
""",
)
},
)
}
}
// 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<OneRegisterInstruction>(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 Fix iOS livestream current time.
hlsCurrentTimeFingerprint.injectLiteralInstructionBooleanCall(
HLS_CURRENT_TIME_FEATURE_FLAG,
"$EXTENSION_CLASS_DESCRIPTOR->fixHLSCurrentTime(Z)Z"
)
// endregion
executeBlock()
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.patches.shared.spoof.streamingdata package app.revanced.patches.youtube.utils.fix.streamingdata
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference import app.revanced.util.getReference
@ -15,37 +15,6 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference
const val STREAMING_DATA_INTERFACE = const val STREAMING_DATA_INTERFACE =
"Lcom/google/protos/youtube/api/innertube/StreamingDataOuterClass${'$'}StreamingData;" "Lcom/google/protos/youtube/api/innertube/StreamingDataOuterClass${'$'}StreamingData;"
internal val buildInitPlaybackRequestFingerprint = legacyFingerprint(
name = "buildInitPlaybackRequestFingerprint",
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
opcodes = listOf(
Opcode.MOVE_RESULT_OBJECT,
Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with.
),
strings = listOf(
"Content-Type",
"Range",
),
)
internal val buildPlayerRequestURIFingerprint = legacyFingerprint(
name = "buildPlayerRequestURIFingerprint",
returnType = "Ljava/lang/String;",
strings = listOf(
"key",
"asig",
),
customFingerprint = { method, _ ->
indexOfToStringInstruction(method) >= 0
},
)
internal fun indexOfToStringInstruction(method: Method) =
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;"
}
internal val buildMediaDataSourceFingerprint = legacyFingerprint( internal val buildMediaDataSourceFingerprint = legacyFingerprint(
name = "buildMediaDataSourceFingerprint", name = "buildMediaDataSourceFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,

View File

@ -1,29 +1,355 @@
package app.revanced.patches.youtube.utils.fix.streamingdata package app.revanced.patches.youtube.utils.fix.streamingdata
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
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.extensions.InstructionExtensions.removeInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
import app.revanced.patches.shared.formatStreamModelConstructorFingerprint
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME 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.patch.PatchList.SPOOF_STREAMING_DATA
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.definingClassOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
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.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch( const val EXTENSION_CLASS_DESCRIPTOR =
{ "$SPOOF_PATH/SpoofStreamingDataPatch;"
compatibleWith(COMPATIBLE_PACKAGE)
dependsOn( val spoofStreamingDataPatch = bytecodePatch(
baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME), SPOOF_STREAMING_DATA.title,
settingsPatch SPOOF_STREAMING_DATA.summary
) {
compatibleWith(COMPATIBLE_PACKAGE)
dependsOn(
settingsPatch,
baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME),
blockRequestPatch,
)
execute {
// region Get replacement streams at player requests.
buildRequestFingerprint.methodOrThrow().apply {
val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this)
val urlRegister =
getInstruction<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
val entrySetIndex = indexOfEntrySetInstruction(this)
val mapRegister = if (entrySetIndex < 0)
urlRegister + 1
else
getInstruction<FiveRegisterInstruction>(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.
val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let {
with(it.method) {
getInstruction<ReferenceInstruction>(it.patternMatch!!.startIndex).reference
}
}
val streamingDataFormatsReference = with(
videoStreamingDataConstructorFingerprint.methodOrThrow(
videoStreamingDataToStringFingerprint
)
) {
val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this)
val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex)
val longMaxValueRegister =
getInstruction<OneRegisterInstruction>(longMaxValueIndex).registerA
val videoIdIndex =
indexOfFirstInstructionOrThrow(longMaxValueIndex) {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.type == "Ljava/lang/String;" &&
reference.definingClass == definingClass
}
val definingClassRegister =
getInstruction<TwoRegisterInstruction>(videoIdIndex).registerB
val videoIdReference =
getInstruction<ReferenceInstruction>(videoIdIndex).reference
addInstructions(
longMaxValueIndex + 1, """
# Get video id.
iget-object v$longMaxValueRegister, v$definingClassRegister, $videoIdReference
# Override approxDurationMs.
invoke-static { v$longMaxValueRegister }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMs(Ljava/lang/String;)J
move-result-wide v$longMaxValueRegister
"""
)
removeInstruction(longMaxValueIndex)
getInstruction<ReferenceInstruction>(getFormatsFieldIndex).reference
}
createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint)
.let { result ->
result.method.apply {
val setStreamDataMethodName = "patch_setStreamingData"
val calcApproxDurationMsMethodName = "patch_calcApproxDurationMs"
val resultClassDef = result.classDef
val resultMethodType = resultClassDef.type
val setStreamingDataIndex = result.patternMatch!!.startIndex
val setStreamingDataField =
getInstruction(setStreamingDataIndex).getReference<FieldReference>()
.toString()
val playerProtoClass =
getInstruction(setStreamingDataIndex + 1).getReference<FieldReference>()!!.definingClass
val protobufClass =
protobufClassParseByteBufferFingerprint.definingClassOrThrow()
val getStreamingDataField = instructions.find { instruction ->
instruction.opcode == Opcode.IGET_OBJECT &&
instruction.getReference<FieldReference>()?.definingClass == playerProtoClass
}?.getReference<FieldReference>()
?: throw PatchException("Could not find getStreamingDataField")
val videoDetailsIndex = result.patternMatch!!.endIndex
val videoDetailsRegister =
getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA
val videoDetailsClass =
getInstruction(videoDetailsIndex).getReference<FieldReference>()!!.type
addInstruction(
videoDetailsIndex + 1,
"invoke-direct { p0, v$videoDetailsRegister }, " +
"$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V",
)
result.classDef.methods.add(
ImmutableMethod(
resultMethodType,
setStreamDataMethodName,
listOf(
ImmutableMethodParameter(
videoDetailsClass,
annotations,
"videoDetails"
)
),
"V",
AccessFlags.PRIVATE.value or AccessFlags.FINAL.value,
annotations,
null,
MutableMethodImplementation(9),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
move-result v0
if-eqz v0, :disabled
# Get video id.
iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String;
if-eqz v2, :disabled
# Get streaming data.
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
move-result-object v3
if-eqz v3, :disabled
# Parse streaming data.
sget-object v4, $playerProtoClass->a:$playerProtoClass
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
move-result-object v5
check-cast v5, $playerProtoClass
iget-object v6, v5, $getStreamingDataField
if-eqz v6, :disabled
# Caculate approxDurationMs.
invoke-direct { p0, v2 }, $resultMethodType->$calcApproxDurationMsMethodName(Ljava/lang/String;)V
# Set spoofed streaming data.
iput-object v6, p0, $setStreamingDataField
:disabled
return-void
""",
)
},
)
resultClassDef.methods.add(
ImmutableMethod(
resultMethodType,
calcApproxDurationMsMethodName,
listOf(
ImmutableMethodParameter(
"Ljava/lang/String;",
annotations,
"videoId"
)
),
"V",
AccessFlags.PRIVATE.value or AccessFlags.FINAL.value,
annotations,
null,
MutableMethodImplementation(12),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
# Get video format list.
iget-object v0, p0, $setStreamingDataField
iget-object v0, v0, $streamingDataFormatsReference
invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;
move-result-object v0
# Initialize approxDurationMs field.
const-wide v1, 0x7fffffffffffffffL
:loop
# Loop over all video formats to get the approxDurationMs
invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z
move-result v3
const-wide/16 v4, 0x0
if-eqz v3, :exit
invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v3
check-cast v3, ${(approxDurationMsReference as FieldReference).definingClass}
# Get approxDurationMs from format
iget-wide v6, v3, $approxDurationMsReference
# Compare with zero to make sure approxDurationMs is not negative
cmp-long v8, v6, v4
if-lez v8, :loop
# Only use the min value of approxDurationMs
invoke-static {v1, v2, v6, v7}, Ljava/lang/Math;->min(JJ)J
move-result-wide v1
goto :loop
:exit
# Save approxDurationMs to integrations
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;J)V
return-void
""",
)
},
)
}
}
// 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<OneRegisterInstruction>(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 Fix iOS livestream current time.
hlsCurrentTimeFingerprint.injectLiteralInstructionBooleanCall(
HLS_CURRENT_TIME_FEATURE_FLAG,
"$EXTENSION_CLASS_DESCRIPTOR->fixHLSCurrentTime(Z)Z"
) )
},
{ // endregion
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
name == "SpoofStreamingData"
}.replaceInstruction(
0,
"const/4 v0, 0x1"
)
addPreference( addPreference(
arrayOf( arrayOf(
"SETTINGS: SPOOF_STREAMING_DATA" "SETTINGS: SPOOF_STREAMING_DATA"
), ),
SPOOF_STREAMING_DATA SPOOF_STREAMING_DATA
) )
} }
) }

View File

@ -76,14 +76,16 @@
<item>6.42.55</item> <item>6.42.55</item>
</string-array> </string-array>
<string-array name="revanced_spoof_client_type_entries"> <string-array name="revanced_spoof_client_type_entries">
<item>@string/revanced_spoof_client_type_entry_ios_music_6_21</item>
<item>@string/revanced_spoof_client_type_entry_android_music_5_29</item>
<item>@string/revanced_spoof_client_type_entry_android_music_4_27</item> <item>@string/revanced_spoof_client_type_entry_android_music_4_27</item>
<item>@string/revanced_spoof_client_type_entry_android_music_5_29</item>
<item>@string/revanced_spoof_client_type_entry_ios_music_6_21</item>
<item>@string/revanced_spoof_client_type_entry_ios_music_7_04</item>
</string-array> </string-array>
<string-array name="revanced_spoof_client_type_entry_values"> <string-array name="revanced_spoof_client_type_entry_values">
<item>IOS_MUSIC_6_21</item>
<item>ANDROID_MUSIC_5_29</item>
<item>ANDROID_MUSIC_4_27</item> <item>ANDROID_MUSIC_4_27</item>
<item>ANDROID_MUSIC_5_29</item>
<item>IOS_MUSIC_6_21</item>
<item>IOS_MUSIC_7_04</item>
</string-array> </string-array>
<string-array name="revanced_spoof_streaming_data_type_entries"> <string-array name="revanced_spoof_streaming_data_type_entries">
<item>@string/revanced_spoof_streaming_data_type_entry_android_vr</item> <item>@string/revanced_spoof_streaming_data_type_entry_android_vr</item>

View File

@ -479,17 +479,7 @@ Info:
<string name="revanced_spoof_client_type_entry_android_music_4_27">Android Music 4.27.53</string> <string name="revanced_spoof_client_type_entry_android_music_4_27">Android Music 4.27.53</string>
<string name="revanced_spoof_client_type_entry_android_music_5_29">Android Music 5.29.53</string> <string name="revanced_spoof_client_type_entry_android_music_5_29">Android Music 5.29.53</string>
<string name="revanced_spoof_client_type_entry_ios_music_6_21">iOS Music 6.21</string> <string name="revanced_spoof_client_type_entry_ios_music_6_21">iOS Music 6.21</string>
<string name="revanced_spoof_client_type_entry_ios_music_7_04">iOS Music 7.04</string>
<string name="revanced_spoof_streaming_data_title">Spoof streaming data</string>
<string name="revanced_spoof_streaming_data_summary">"Spoof the streaming data to prevent playback issues.
• When used with 'Spoof client', playback issues may occur."</string>
<string name="revanced_spoof_streaming_data_type_title">Default client</string>
<string name="revanced_spoof_streaming_data_type_summary">Defines a default client that fetches streaming data.</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_title">Show in Stats for nerds</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary">Shows the client used to fetch streaming data in Stats for nerds.</string>
<string name="revanced_spoof_streaming_data_type_entry_android_vr">Android VR</string>
<string name="revanced_spoof_streaming_data_type_entry_android_music">Android Music</string>
<string name="revanced_default_app_settings_title">Open default app settings</string> <string name="revanced_default_app_settings_title">Open default app settings</string>
<string name="revanced_default_app_settings_summary">To open YouTube Music links in RVX Music, enable Open supported links and enable all the Supported web addresses.</string> <string name="revanced_default_app_settings_summary">To open YouTube Music links in RVX Music, enable Open supported links and enable all the Supported web addresses.</string>