diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java new file mode 100644 index 0000000000..e99d5dc6b8 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -0,0 +1,77 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowAV1; +import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowVP9; + +import android.os.Build; + +import app.revanced.integrations.shared.Utils; + +public enum ClientType { + // https://dumps.tadiphone.dev/dumps/oculus/eureka + IOS(5, + // iPhone 15 supports AV1 hardware decoding. + // Only use if this Android device also has hardware decoding. + allowAV1() + ? "iPhone16,2" // 15 Pro Max + : "iPhone11,4", // XS Max + // iOS 14+ forces VP9. + allowVP9() + ? "17.5.1.21F90" + : "13.7.17H35", + allowVP9() + ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" + : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", + // Version number should be a valid iOS release. + // https://www.ipa4fun.com/history/185230 + "19.10.7" + ), + ANDROID_VR(28, + "Quest 3", + "12", + "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", + "1.56.21" + ), + @Deprecated() // Android spoofing in this context no longer works. + ANDROID(3, + Build.MODEL, + Build.VERSION.RELEASE, + String.format("com.google.android.youtube/%s (Linux; U; Android %s; GB) gzip", + Utils.getAppVersionName(), Build.VERSION.RELEASE), + Utils.getAppVersionName() + ); + + /** + * YouTube + * client type + */ + public final int id; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String model; + + /** + * Device OS version. + */ + public final String osVersion; + + /** + * Player user-agent. + */ + public final String userAgent; + + /** + * App version. + */ + public final String appVersion; + + ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { + this.id = id; + this.model = model; + this.osVersion = osVersion; + this.userAgent = userAgent; + this.appVersion = appVersion; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java new file mode 100644 index 0000000000..3d2cb7911c --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -0,0 +1,63 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.youtube.settings.Settings; + +public class DeviceHardwareSupport { + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); + + private static boolean deviceHasVP9HardwareDecoding() { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + ? codecInfo.isHardwareAccelerated() + : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. + if (isHardwareAccelerated && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { + Logger.printDebug(() -> "Device supports VP9 hardware decoding."); + return true; + } + } + } + } + + Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); + return false; + } + + private static boolean deviceHasAV1HardwareDecoding() { + // It appears all devices with hardware AV1 are also Android 10 or newer. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/av01")) { + Logger.printDebug(() -> "Device supports AV1 hardware decoding."); + return true; + } + } + } + } + } + + Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); + return false; + } + + public static boolean allowVP9() { + return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_FORCE_AVC.get(); + } + + public static boolean allowAV1() { + return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index e8746c18da..faadb781f7 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -1,12 +1,6 @@ package app.revanced.integrations.youtube.patches.spoof; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowAV1; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowVP9; - -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; import android.net.Uri; -import android.os.Build; import androidx.annotation.Nullable; @@ -29,10 +23,10 @@ @SuppressWarnings("unused") public class SpoofClientPatch { private static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get(); - private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); - private static final boolean SPOOF_IOS = SPOOF_CLIENT && SPOOF_CLIENT_TYPE == ClientType.IOS; - - private static final boolean SPOOF_STREAM = Settings.SPOOF_STREAM.get(); + private static final SpoofClientStrategy SPOOF_STRATEGY = Settings.SPOOF_CLIENT_STRATEGY.get(); + @Nullable + private static final ClientType SPOOF_CLIENT_TYPE = SPOOF_CLIENT ? null : SPOOF_STRATEGY.clientType; + private static final boolean SPOOF_STREAM = SPOOF_CLIENT && SPOOF_STRATEGY == SpoofClientStrategy.REPLACE_STREAMS; /** * Any unreachable ip address. Used to intentionally fail requests. @@ -50,7 +44,7 @@ public class SpoofClientPatch { * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. */ public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT || SPOOF_STREAM) { + if (SPOOF_CLIENT) { try { String path = playerRequestUri.getPath(); @@ -73,7 +67,7 @@ public static Uri blockGetWatchRequest(Uri playerRequestUri) { * Blocks /initplayback requests. */ public static String blockInitPlaybackRequest(String originalUrlString) { - if (SPOOF_CLIENT || SPOOF_STREAM) { + if (SPOOF_CLIENT) { try { var originalUri = Uri.parse(originalUrlString); String path = originalUri.getPath(); @@ -94,51 +88,51 @@ public static String blockInitPlaybackRequest(String originalUrlString) { /** * Injection point. */ - public static int getClientTypeId(int originalClientTypeId) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; + public static boolean isClientTypeSpoofingEnabled() { + return SPOOF_CLIENT_TYPE != null; } /** * Injection point. */ - public static String getClientVersion(String originalClientVersion) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; + public static boolean isSpoofStreamEnabled() { + return SPOOF_STREAM; } /** * Injection point. */ - public static String getClientModel(String originalClientModel) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.model : originalClientModel; + public static int getClientTypeId(int originalClientTypeId) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; } /** * Injection point. - * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. */ - public static String getOsVersion(String originalOsVersion) { - return SPOOF_CLIENT ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; + public static String getClientVersion(String originalClientVersion) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; } /** * Injection point. */ - public static boolean enablePlayerGesture(boolean original) { - return SPOOF_CLIENT || original; + public static String getClientModel(String originalClientModel) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.model : originalClientModel; } /** * Injection point. + * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. */ - public static boolean isClientSpoofingEnabled() { - return SPOOF_CLIENT; + public static String getOsVersion(String originalOsVersion) { + return SPOOF_CLIENT_TYPE != null ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; } /** * Injection point. */ - public static boolean isSpoofStreamEnabled() { - return SPOOF_STREAM; + public static boolean enablePlayerGesture(boolean original) { + return SPOOF_STRATEGY.enablePlayerGesture || original; } /** @@ -147,7 +141,7 @@ public static boolean isSpoofStreamEnabled() { * Return true to force create the playback speed menu. */ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { - return SPOOF_IOS || original; + return SPOOF_STRATEGY.forceCreatePlaybackSpeedMenu || original; } /** @@ -156,7 +150,7 @@ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { * Return true to force enable audio background play. */ public static boolean overrideBackgroundAudioPlayback() { - return SPOOF_IOS && BackgroundPlaybackPatch.playbackIsNotShort(); + return SPOOF_STRATEGY.overrideBackgroundAudioPlayback && BackgroundPlaybackPatch.playbackIsNotShort(); } /** @@ -164,14 +158,15 @@ public static boolean overrideBackgroundAudioPlayback() { */ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url, Map playerHeaders) { - if (SPOOF_CLIENT || SPOOF_STREAM) { + if (SPOOF_CLIENT) { Uri uri = Uri.parse(url); String path = uri.getPath(); if (path != null && path.contains("player") && !path.contains("heartbeat")) { - if (SPOOF_CLIENT) { + if (SPOOF_CLIENT_TYPE != null) { Logger.printDebug(() -> "Overriding user agent for /player call"); builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent); } else { + // Spoof stream. String videoId = uri.getQueryParameter("id"); currentVideoStream = StreamingDataRequester.fetch(videoId, playerHeaders); } @@ -240,136 +235,43 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos return postData; } - // Must check for device features in a separate class and cannot place this code inside - // the Patch or ClientType enum due to cyclic Setting references. - static class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); - - private static boolean deviceHasVP9HardwareDecoding() { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - ? codecInfo.isHardwareAccelerated() - : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. - if (isHardwareAccelerated && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - Logger.printDebug(() -> "Device supports VP9 hardware decoding."); - return true; - } - } - } - } - - Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); - return false; - } - - private static boolean deviceHasAV1HardwareDecoding() { - // It appears all devices with hardware AV1 are also Android 10 or newer. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/av01")) { - Logger.printDebug(() -> "Device supports AV1 hardware decoding."); - return true; - } - } - } - } - } - - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); - return false; - } - - static boolean allowVP9() { - return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get(); - } - - static boolean allowAV1() { - return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + public enum SpoofClientStrategy { + REPLACE_STREAMS(null, + false, + false, + false), + CLIENT_IOS(ClientType.IOS, + true, + true, + true), + CLIENT_ANDROID_VR(ClientType.ANDROID_VR, + false, + false, + false); + + private final ClientType clientType; + private final boolean forceCreatePlaybackSpeedMenu; + private final boolean overrideBackgroundAudioPlayback; + private final boolean enablePlayerGesture; + + SpoofClientStrategy(ClientType clientType, + boolean forceCreatePlaybackSpeedMenu, + boolean overrideBackgroundAudioPlayback, + boolean enablePlayerGesture) { + this.clientType = clientType; + this.forceCreatePlaybackSpeedMenu = forceCreatePlaybackSpeedMenu; + this.overrideBackgroundAudioPlayback = overrideBackgroundAudioPlayback; + this.enablePlayerGesture = enablePlayerGesture; } } - public enum ClientType { - // https://dumps.tadiphone.dev/dumps/oculus/eureka - IOS(5, - // iPhone 15 supports AV1 hardware decoding. - // Only use if this Android device also has hardware decoding. - allowAV1() - ? "iPhone16,2" // 15 Pro Max - : "iPhone11,4", // XS Max - // iOS 14+ forces VP9. - allowVP9() - ? "17.5.1.21F90" - : "13.7.17H35", - allowVP9() - ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" - : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", - // Version number should be a valid iOS release. - // https://www.ipa4fun.com/history/185230 - "19.10.7" - ), - ANDROID_VR(28, - "Quest 3", - "12", - "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "1.56.21" - ), - ANDROID(3, - Build.MODEL, - Build.VERSION.RELEASE, - String.format("com.google.android.youtube/%s (Linux; U; Android %s; GB) gzip", - Utils.getAppVersionName(), Build.VERSION.RELEASE), - Utils.getAppVersionName() - ); - - /** - * YouTube - * client type - */ - public final int id; - - /** - * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) - */ - public final String model; - - /** - * Device OS version. - */ - public final String osVersion; - - /** - * Player user-agent. - */ - public final String userAgent; - - /** - * App version. - */ - public final String appVersion; - - ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { - this.id = id; - this.model = model; - this.osVersion = osVersion; - this.userAgent = userAgent; - this.appVersion = appVersion; - } - } - - public static final class ForceiOSAVCAvailability implements Setting.Availability { + public static final class ForceAVCAvailability implements Setting.Availability { @Override public boolean isAvailable() { - return (Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS) - || Settings.SPOOF_STREAM.get(); + if (!Settings.SPOOF_CLIENT.get()) return false; + + SpoofClientStrategy strategy = Settings.SPOOF_CLIENT_STRATEGY.get(); + return strategy == SpoofClientStrategy.CLIENT_IOS || strategy == SpoofClientStrategy.REPLACE_STREAMS; } } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index fd0977d8a5..b3c590d6ef 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -1,6 +1,6 @@ package app.revanced.integrations.youtube.patches.spoof.requests; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; +import app.revanced.integrations.youtube.patches.spoof.ClientType; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.youtube.requests.Route; import app.revanced.integrations.shared.Logger; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java index 200d5fb0d1..b43f040f35 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java @@ -3,8 +3,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.ClientType; import app.revanced.integrations.youtube.patches.spoof.StoryboardRenderer; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java index e00b333006..e5e102a900 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequester.java @@ -20,7 +20,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; +import app.revanced.integrations.youtube.patches.spoof.ClientType; public class StreamingDataRequester { diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 077811523c..d99517f306 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -6,7 +6,7 @@ import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.SpoofClientStrategy; import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; import java.util.Arrays; @@ -256,9 +256,9 @@ public class Settings extends BaseSettings { public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true,"revanced_spoof_client_user_dialog_message"); - public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true, - "revanced_spoof_client_ios_force_avc_user_dialog_message", new SpoofClientPatch.ForceiOSAVCAvailability()); - public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT)); + public static final BooleanSetting SPOOF_CLIENT_FORCE_AVC = new BooleanSetting("revanced_spoof_client_force_avc", FALSE, true, + "revanced_spoof_client_force_avc_user_dialog_message", new SpoofClientPatch.ForceAVCAvailability()); + public static final EnumSetting SPOOF_CLIENT_STRATEGY = new EnumSetting<>("revanced_spoof_client_strategy", SpoofClientStrategy.REPLACE_STREAMS, true, parent(SPOOF_CLIENT)); public static final BooleanSetting SPOOF_STREAM = new BooleanSetting("revanced_spoof_stream", FALSE, true); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", "");