diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java index 5c60437b5..f6d248715 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java @@ -2,6 +2,9 @@ package app.revanced.extension.music.patches.spoof; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48; +import static app.revanced.extension.shared.spoof.ClientType.VISIONOS; + +import java.util.List; import app.revanced.extension.shared.spoof.ClientType; import app.revanced.extension.shared.spoof.requests.StreamingDataRequest; @@ -13,10 +16,11 @@ public class SpoofVideoStreamsPatch { * Injection point. */ public static void setClientOrderToUse() { - ClientType[] availableClients = { + List availableClients = List.of( ANDROID_VR_1_43_32, ANDROID_VR_1_61_48, - }; + VISIONOS + ); StreamingDataRequest.setClientOrderToUse(availableClients, ANDROID_VR_1_43_32); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index e2c70f616..2cb08ff90 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -4,7 +4,6 @@ import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static app.revanced.extension.shared.settings.Setting.parent; import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability; -import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability; import app.revanced.extension.shared.spoof.ClientType; @@ -31,8 +30,6 @@ public class BaseSettings { public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message"); public static final EnumSetting SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability()); public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS)); - public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true, - "revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability()); // Client type must be last spoof setting due to cyclic references. public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_61_48, true, parent(SPOOF_VIDEO_STREAMS)); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java index cc80cb924..d29b293ff 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java @@ -2,15 +2,19 @@ package app.revanced.extension.shared.spoof; import android.os.Build; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Locale; import java.util.Objects; import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.settings.BaseSettings; public enum ClientType { + /** + * Video not playable: Kids / Paid / Movie / Private / Age-restricted. + * This client can only be used when logged out. + */ // https://dumps.tadiphone.dev/dumps/oculus/eureka ANDROID_VR_1_61_48( 28, @@ -29,27 +33,30 @@ public enum ClientType { false, "Android VR 1.61" ), - // Chromecast with Google TV 4K. - // https://dumps.tadiphone.dev/dumps/google/kirkwood - ANDROID_UNPLUGGED( - 29, - "ANDROID_UNPLUGGED", - "com.google.android.apps.youtube.unplugged", - "Google", - "Google TV Streamer", - "Android", - "14", - "34", - "UTT3.240625.001.K5", - "132.0.6808.3", - "8.49.0", - true, - true, - "Android TV" + /** + * Uses non adaptive bitrate, which fixes audio stuttering with YT Music. + * Does not use AV1. + */ + ANDROID_VR_1_43_32( + ANDROID_VR_1_61_48.id, + ANDROID_VR_1_61_48.clientName, + Objects.requireNonNull(ANDROID_VR_1_61_48.packageName), + ANDROID_VR_1_61_48.deviceMake, + ANDROID_VR_1_61_48.deviceModel, + ANDROID_VR_1_61_48.osName, + ANDROID_VR_1_61_48.osVersion, + Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion), + Objects.requireNonNull(ANDROID_VR_1_61_48.buildId), + "107.0.5284.2", + "1.43.32", + ANDROID_VR_1_61_48.requiresAuth, + ANDROID_VR_1_61_48.useAuth, + "Android VR 1.43" ), - // Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children". - // Google Pixel 9 Pro Fold - // https://dumps.tadiphone.dev/dumps/google/barbet + /** + * Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children". + * Google Pixel 9 Pro Fold + */ ANDROID_CREATOR( 14, "ANDROID_CREATOR", @@ -66,62 +73,22 @@ public enum ClientType { true, "Android Creator" ), - IOS_UNPLUGGED( - 33, - "IOS_UNPLUGGED", - "com.google.ios.youtubeunplugged", - "Apple", - forceAVC() - // 11 Pro Max (last device with iOS 13) - ? "iPhone12,5" - // 15 Pro Max - : "iPhone16,2", - "iOS", - forceAVC() - // iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1. - ? "13.7.17H35" - : "18.2.22C152", - null, - null, - null, - // Version number should be a valid iOS release. - // https://www.ipa4fun.com/history/152043/ - forceAVC() - // Some newer versions can also force AVC, - // but 6.45 is the last version that supports iOS 13. - ? "6.45" - : "8.49", - true, - true, - forceAVC() - ? "iOS TV Force AVC" - : "iOS TV" - ), /** - * Uses non adaptive bitrate, which fixes audio stuttering with YT Music. - * Uses VP9 and not AV1. + * Internal YT client for an unreleased YT client. May stop working at any time. */ - ANDROID_VR_1_43_32( - ANDROID_VR_1_61_48.id, - ANDROID_VR_1_61_48.clientName, - ANDROID_VR_1_61_48.packageName, - ANDROID_VR_1_61_48.deviceMake, - ANDROID_VR_1_61_48.deviceModel, - ANDROID_VR_1_61_48.osName, - ANDROID_VR_1_61_48.osVersion, - ANDROID_VR_1_61_48.androidSdkVersion, - ANDROID_VR_1_61_48.buildId, - "107.0.5284.2", - "1.43.32", - ANDROID_VR_1_61_48.requiresAuth, - ANDROID_VR_1_61_48.useAuth, - "Android VR 1.43" + VISIONOS(101, + "VISIONOS", + "Apple", + "RealityDevice14,1", + "visionOS", + "1.3.21O771", + "0.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", + false, + false, + "visionOS" ); - private static boolean forceAVC() { - return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get(); - } - /** * YouTube * client type @@ -133,6 +100,7 @@ public enum ClientType { /** * App package name. */ + @Nullable private final String packageName; /** @@ -202,17 +170,20 @@ public enum ClientType { */ public final String friendlyName; + /** + * Android constructor. + */ @SuppressWarnings("ConstantLocale") ClientType(int id, String clientName, - String packageName, + @NonNull String packageName, String deviceMake, String deviceModel, String osName, String osVersion, - @Nullable String androidSdkVersion, - @Nullable String buildId, - @Nullable String cronetVersion, + @NonNull String androidSdkVersion, + @NonNull String buildId, + @NonNull String cronetVersion, String clientVersion, boolean requiresAuth, boolean useAuth, @@ -233,31 +204,44 @@ public enum ClientType { this.friendlyName = friendlyName; Locale defaultLocale = Locale.getDefault(); - if (androidSdkVersion == null) { - // Convert version from '18.2.22C152' into '18_2_22' - String userAgentOsVersion = osVersion - .replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1") - .replace(".", "_"); - // https://github.com/mitmproxy/mitmproxy/issues/4836 - this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)", - packageName, - clientVersion, - deviceModel, - userAgentOsVersion, - defaultLocale - ); - } else { - this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)", - packageName, - clientVersion, - osVersion, - defaultLocale, - deviceModel, - Objects.requireNonNull(buildId), - Objects.requireNonNull(cronetVersion) - ); - } + this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)", + packageName, + clientVersion, + osVersion, + defaultLocale, + deviceModel, + Objects.requireNonNull(buildId), + Objects.requireNonNull(cronetVersion) + ); Logger.printDebug(() -> "userAgent: " + this.userAgent); } + @SuppressWarnings("ConstantLocale") + ClientType(int id, + String clientName, + String deviceMake, + String deviceModel, + String osName, + String osVersion, + String clientVersion, + String userAgent, + boolean requiresAuth, + boolean useAuth, + String friendlyName) { + this.id = id; + this.clientName = clientName; + this.deviceMake = deviceMake; + this.deviceModel = deviceModel; + this.osName = osName; + this.osVersion = osVersion; + this.clientVersion = clientVersion; + this.userAgent = userAgent; + this.requiresAuth = requiresAuth; + this.useAuth = useAuth; + this.friendlyName = friendlyName; + this.packageName = null; + this.androidSdkVersion = null; + this.buildId = null; + this.cronetVersion = null; + } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java index a23814056..13f6a1d0b 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java @@ -10,6 +10,7 @@ import java.util.Map; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.spoof.requests.StreamingDataRequest; @@ -19,7 +20,10 @@ public class SpoofVideoStreamsPatch { private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get(); private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA - && BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED; + && BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.VISIONOS; + + @Nullable + private static volatile AppLanguage languageOverride; /** * Domain used for internet connectivity verification. @@ -43,10 +47,21 @@ public class SpoofVideoStreamsPatch { return false; // Modified during patching. } - public static boolean notSpoofingToAndroid() { - return !isPatchIncluded() - || !BaseSettings.SPOOF_VIDEO_STREAMS.get() - || BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED; + public static boolean spoofingToClientWithNoMultiAudioStreams() { + return isPatchIncluded() && BaseSettings.SPOOF_VIDEO_STREAMS.get(); + } + + /** + * @param language Language override for non-authenticated requests. If this is null then + * {@link BaseSettings#SPOOF_VIDEO_STREAMS_LANGUAGE} is used. + */ + public static void setLanguageOverride(@Nullable AppLanguage language) { + languageOverride = language; + } + + @Nullable + public static AppLanguage getLanguageOverride() { + return languageOverride; } /** @@ -261,17 +276,8 @@ public class SpoofVideoStreamsPatch { public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability { @Override public boolean isAvailable() { - ClientType clientType = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); - return BaseSettings.SPOOF_VIDEO_STREAMS.get() - && (clientType == ClientType.ANDROID_VR_1_61_48 || clientType == ClientType.ANDROID_VR_1_43_32); - } - } - - public static final class SpoofiOSAvailability implements Setting.Availability { - @Override - public boolean isAvailable() { - return BaseSettings.SPOOF_VIDEO_STREAMS.get() - && BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED; + // Since all current clients are un-authenticated, this works for all spoof clients. + return BaseSettings.SPOOF_VIDEO_STREAMS.get(); } } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java index 6cc3ec1cd..82db445d7 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java @@ -1,5 +1,7 @@ package app.revanced.extension.shared.spoof.requests; +import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32; + import org.json.JSONException; import org.json.JSONObject; @@ -10,8 +12,10 @@ import java.util.Locale; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.spoof.ClientType; +import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch; final class PlayerRoutes { static final Route.CompiledRoute GET_STREAMING_DATA = new Route( @@ -37,15 +41,16 @@ final class PlayerRoutes { try { JSONObject context = new JSONObject(); - // Can override default language only if no login is used. - // Could use preferred audio for all clients that do not login, - // but if this is a fall over client it will set the language even though - // the audio language is not selectable in the UI. - ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); - Locale streamLocale = (userSelectedClient == ClientType.ANDROID_VR_1_61_48 - || userSelectedClient == ClientType.ANDROID_VR_1_43_32) - ? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale() - : Locale.getDefault(); + AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride(); + if (language == null || BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ANDROID_VR_1_43_32) { + // Force original audio has not overrode the language. + // Or if YT has fallen over to the very last client (VR 1.43), then always + // use the app language because forcing an audio stream of specific languages + // can sometimes fail so it's better to try and load something rather than nothing. + language = BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get(); + } + //noinspection ExtractMethodRecommender + Locale streamLocale = language.getLocale(); JSONObject client = new JSONObject(); client.put("deviceMake", clientType.deviceMake); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java index d814ced88..81e96b690 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java @@ -37,10 +37,15 @@ public class StreamingDataRequest { private static volatile ClientType[] clientOrderToUse = ClientType.values(); - public static void setClientOrderToUse(ClientType[] availableClients, ClientType preferredClient) { - Objects.requireNonNull(availableClients); + public static void setClientOrderToUse(List availableClients, ClientType preferredClient) { + Objects.requireNonNull(preferredClient); - clientOrderToUse = new ClientType[availableClients.length]; + int availableClientSize = availableClients.size(); + if (!availableClients.contains(preferredClient)) { + availableClientSize++; + } + + clientOrderToUse = new ClientType[availableClientSize]; clientOrderToUse[0] = preferredClient; int i = 1; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java index c67a5ffec..02149115c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ForceOriginalAudioPatch.java @@ -1,7 +1,7 @@ package app.revanced.extension.youtube.patches; import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch; import app.revanced.extension.youtube.settings.Settings; @@ -11,16 +11,20 @@ public class ForceOriginalAudioPatch { private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4"; /** - * If the conditions to use this patch were present when the app launched. + * Injection point. */ - public static boolean PATCH_AVAILABLE = SpoofVideoStreamsPatch.notSpoofingToAndroid(); - - public static final class ForceOriginalAudioAvailability implements Setting.Availability { - @Override - public boolean isAvailable() { - // Check conditions of launch and now. Otherwise if spoofing is changed - // without a restart the setting will show as available when it's not. - return PATCH_AVAILABLE && SpoofVideoStreamsPatch.notSpoofingToAndroid(); + public static void setPreferredLanguage() { + if (Settings.FORCE_ORIGINAL_AUDIO.get()) { + // None of the current spoof clients support audio track menu, + // And all are un-authenticated and can request any language code + // (authenticated requests ignore the language code and always use the account language). + // To still support force original audio, if it's enabled then pick a language + // that is not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972 + // but the language is also supported natively by the Meta Quest device that + // Android VR is spoofing. + AppLanguage override = AppLanguage.SV; + Logger.printDebug(() -> "Setting language override: " + override); + SpoofVideoStreamsPatch.setLanguageOverride(override); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java index 2408ac81c..cbf2930bb 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java @@ -9,13 +9,13 @@ import app.revanced.extension.youtube.shared.ShortsPlayerState; public class PlayerFlyoutMenuItemsFilter extends Filter { public static final class HideAudioFlyoutMenuAvailability implements Setting.Availability { - private static final boolean AVAILABLE_ON_LAUNCH = SpoofVideoStreamsPatch.notSpoofingToAndroid(); + private static final boolean AVAILABLE_ON_LAUNCH = !SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams(); @Override public boolean isAvailable() { // Check conditions of launch and now. Otherwise if spoofing is changed // without a restart the setting will show as available when it's not. - return AVAILABLE_ON_LAUNCH && SpoofVideoStreamsPatch.notSpoofingToAndroid(); + return AVAILABLE_ON_LAUNCH && !SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams(); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java index 23a415708..df228e288 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java @@ -1,9 +1,11 @@ package app.revanced.extension.youtube.patches.spoof; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_CREATOR; -import static app.revanced.extension.shared.spoof.ClientType.ANDROID_UNPLUGGED; +import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32; import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48; -import static app.revanced.extension.shared.spoof.ClientType.IOS_UNPLUGGED; +import static app.revanced.extension.shared.spoof.ClientType.VISIONOS; + +import java.util.List; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.spoof.ClientType; @@ -16,12 +18,13 @@ public class SpoofVideoStreamsPatch { * Injection point. */ public static void setClientOrderToUse() { - ClientType[] availableClients = { + List availableClients = List.of( ANDROID_VR_1_61_48, - ANDROID_UNPLUGGED, ANDROID_CREATOR, - IOS_UNPLUGGED - }; + VISIONOS, + // VR 1.43 must be last as spoof streaming data handles it slightly differently. + ANDROID_VR_1_43_32 + ); StreamingDataRequest.setClientOrderToUse(availableClients, BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index 2c089bb69..d40879a3c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -12,7 +12,6 @@ import static app.revanced.extension.youtube.patches.ChangeHeaderPatch.HeaderLog import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability; import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage; import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode; -import static app.revanced.extension.youtube.patches.ForceOriginalAudioPatch.ForceOriginalAudioAvailability; import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHorizontalDragAvailability; import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MINIMAL; @@ -75,7 +74,7 @@ public class Settings extends BaseSettings { "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.5\n3.0\n4.0\n5.0\n6.0\n7.0\n8.0", true); // Audio - public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, new ForceOriginalAudioAvailability()); + public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, true); // Ads public static final BooleanSetting HIDE_CREATOR_STORE_SHELF = new BooleanSetting("revanced_hide_creator_store_shelf", TRUE); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ForceOriginalAudioSwitchPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ForceOriginalAudioSwitchPreference.java deleted file mode 100644 index 6a8c3af36..000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ForceOriginalAudioSwitchPreference.java +++ /dev/null @@ -1,36 +0,0 @@ -package app.revanced.extension.youtube.settings.preference; - -import static app.revanced.extension.shared.StringRef.str; - -import android.content.Context; -import android.preference.SwitchPreference; -import android.util.AttributeSet; - -import app.revanced.extension.youtube.patches.ForceOriginalAudioPatch; - -@SuppressWarnings({"deprecation", "unused"}) -public class ForceOriginalAudioSwitchPreference extends SwitchPreference { - - { - if (!ForceOriginalAudioPatch.PATCH_AVAILABLE) { - // Show why force audio is not available. - String summary = str("revanced_force_original_audio_not_available"); - setSummary(summary); - setSummaryOn(summary); - setSummaryOff(summary); - } - } - - public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - public ForceOriginalAudioSwitchPreference(Context context) { - super(context); - } -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HideAudioFlyoutMenuPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HideAudioFlyoutMenuPreference.java index 4d67360a7..c73377fd5 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HideAudioFlyoutMenuPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HideAudioFlyoutMenuPreference.java @@ -12,8 +12,8 @@ import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch; public class HideAudioFlyoutMenuPreference extends SwitchPreference { { - // Audio menu is not available if spoofing to Android client type. - if (!SpoofVideoStreamsPatch.notSpoofingToAndroid()) { + // Audio menu is not available if spoofing to most client types. + if (SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams()) { String summary = str("revanced_hide_player_flyout_audio_track_not_available"); setSummary(summary); setSummaryOn(summary); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java index 33a69b6b0..07d5c9d6b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java @@ -78,21 +78,17 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference { Logger.printDebug(() -> "Updating spoof stream side effects preference"); setEnabled(BaseSettings.SPOOF_VIDEO_STREAMS.get()); - String key = "revanced_spoof_video_streams_about_" + - (clientType == ClientType.IOS_UNPLUGGED - ? "ios_tv" - : "android"); - String title = str(key + "_title"); - String summary = str(key + "_summary"); - - // Android VR supports AV1 but all other clients do not. - if (clientType != ClientType.ANDROID_VR_1_61_48 - && clientType != ClientType.ANDROID_VR_1_43_32) { - summary += '\n' + str("revanced_spoof_video_streams_about_no_av1"); - } - + String title = str("revanced_spoof_video_streams_about_title"); + // Currently only Android VR and VisionOS are supported, and both have the same base side effects. + String summary = str("revanced_spoof_video_streams_about_android_summary"); summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos"); + if (clientType == ClientType.VISIONOS) { + summary = str("revanced_spoof_video_streams_about_experimental") + + '\n' + summary + + '\n' + str("revanced_spoof_video_streams_about_no_av1"); + } + setTitle(title); setSummary(summary); } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt index 82f19f81a..ed2f7f043 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -89,7 +89,7 @@ val downloadsPatch = bytecodePatch( // Main activity is used to launch downloader intent. mainActivityOnCreateFingerprint.method.addInstruction( - 1, + 0, "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V" ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt index 25d291809..24e062d40 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt @@ -1,6 +1,7 @@ package app.revanced.patches.youtube.layout.seekbar import app.revanced.patcher.fingerprint +import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE import app.revanced.util.containsLiteralInstruction import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstruction @@ -103,7 +104,7 @@ internal val launchScreenLayoutTypeFingerprint = fingerprint { custom { method, _ -> val firstParameter = method.parameterTypes.firstOrNull() // 19.25 - 19.45 - (firstParameter == "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;" + (firstParameter == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE || firstParameter == "Landroid/app/Activity;") // 19.46+ && method.containsLiteralInstruction(launchScreenLayoutTypeLotteFeatureFlag) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 470bdc0b5..bf63a2191 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -68,7 +68,7 @@ val shortsAutoplayPatch = bytecodePatch( // Main activity is used to check if app is in pip mode. mainActivityOnCreateFingerprint.method.addInstruction( - 1, + 0, "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->setMainActivity(Landroid/app/Activity;)V", ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt index 56abd43df..aa9aa1954 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt @@ -90,8 +90,8 @@ val openShortsInRegularPlayerPatch = bytecodePatch( // Activity is used as the context to launch an Intent. mainActivityOnCreateFingerprint.method.addInstruction( - 1, - "invoke-static/range { p0 .. p0 }, ${EXTENSION_CLASS_DESCRIPTOR}->" + + 0, + "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" + "setMainActivity(Landroid/app/Activity;)V", ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt index 8864c20c6..583750c06 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt @@ -1,6 +1,7 @@ package app.revanced.patches.youtube.layout.theme import app.revanced.patcher.fingerprint +import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -37,6 +38,6 @@ internal val splashScreenStyleFingerprint = fingerprint { parameters("Landroid/os/Bundle;") literal { SPLASH_SCREEN_STYLE_FEATURE_FLAG } custom { method, classDef -> - method.name == "onCreate" && classDef.endsWith("/MainActivity;") + method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt index 2e0ae6165..5a7d976df 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt @@ -40,9 +40,7 @@ val announcementsPatch = bytecodePatch( ) mainActivityOnCreateFingerprint.method.addInstruction( - // Insert index must be greater than the insert index used by GmsCoreSupport, - // as both patch the same method and GmsCore check should be first. - 1, + 0, "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V", ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt index 8b60589ce..f9ee7849b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt @@ -34,12 +34,7 @@ val checkWatchHistoryDomainNameResolutionPatch = bytecodePatch( addResources("youtube", "misc.dns.checkWatchHistoryDomainNameResolutionPatch") mainActivityOnCreateFingerprint.method.addInstruction( - // FIXME: Insert index must be greater than the insert index used by GmsCoreSupport, - // as both patch the same method and GmsCoreSupport check should be first, - // but the patch does not depend on GmsCoreSupport, so it should not be possible to enforce this - // unless a third patch is added that this patch and GmsCoreSupport depend on to manage - // the order of the patches. - 1, + 0, "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->checkDnsResolver(Landroid/app/Activity;)V", ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/SharedExtensionPatch.kt index 553cc486f..af4b62e4c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/SharedExtensionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/SharedExtensionPatch.kt @@ -3,4 +3,5 @@ package app.revanced.patches.youtube.misc.extension import app.revanced.patches.shared.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.hooks.* -val sharedExtensionPatch = sharedExtensionPatch("youtube", applicationInitHook) +val sharedExtensionPatch = sharedExtensionPatch("youtube", + applicationInitHook, applicationInitOnCrateHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/hooks/ApplicationInitHook.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/hooks/ApplicationInitHook.kt index 6a0e7d1f4..743d1162d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/hooks/ApplicationInitHook.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/hooks/ApplicationInitHook.kt @@ -1,11 +1,23 @@ package app.revanced.patches.youtube.misc.extension.hooks import app.revanced.patches.shared.misc.extension.extensionHook +import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE /** * Hooks the context when the app is launched as a regular application (and is not an embedded video playback). */ // Extension context is the Activity itself. internal val applicationInitHook = extensionHook { + // Does _not_ resolve to the YouTube main activity. + // Required as some hooked code runs before the main activity is launched. strings("Application creation", "Application.onCreate") } + +internal val applicationInitOnCrateHook = extensionHook { + returns("V") + parameters("Landroid/os/Bundle;") + custom { method, classDef -> + method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt index f1ea33327..ee6750c60 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt @@ -127,8 +127,7 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig // Litho filtering based on navigation tab before the tab is updated. mainActivityOnBackPressedFingerprint.method.addInstruction( 0, - "invoke-static { p0 }, " + - "$EXTENSION_CLASS_DESCRIPTOR->onBackPressed(Landroid/app/Activity;)V", + "invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->onBackPressed(Landroid/app/Activity;)V", ) // Hook the search bar. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt index dbf055299..4a971e079 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt @@ -69,14 +69,13 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch( entryValuesKey = "revanced_language_entry_values", tag = "app.revanced.extension.shared.settings.preference.SortedListPreference" ), - SwitchPreference("revanced_spoof_video_streams_ios_force_avc"), SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"), - ), - ), + ) + ) ) mainActivityOnCreateFingerprint.method.addInstruction( - 1, // Must use 1 index so context is set by extension patch., + 0, "invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setClientOrderToUse()V" ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt index 4a9f3a020..6fc138fc5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt @@ -4,6 +4,8 @@ import app.revanced.patcher.fingerprint import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +internal const val YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE = "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;" + internal val conversionContextFingerprintToString = fingerprint { parameters() strings( @@ -48,7 +50,7 @@ internal val mainActivityConstructorFingerprint = fingerprint { accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) parameters() custom { _, classDef -> - classDef.endsWith("/MainActivity;") + classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE } } @@ -57,7 +59,7 @@ internal val mainActivityOnBackPressedFingerprint = fingerprint { returns("V") parameters() custom { method, classDef -> - method.name == "onBackPressed" && classDef.endsWith("/MainActivity;") + method.name == "onBackPressed" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE } } @@ -65,7 +67,7 @@ internal val mainActivityOnCreateFingerprint = fingerprint { returns("V") parameters("Landroid/os/Bundle;") custom { method, classDef -> - method.name == "onCreate" && classDef.endsWith("/MainActivity;") + method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt index 8a3ee4e92..b17d1c96c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt @@ -1,5 +1,6 @@ package app.revanced.patches.youtube.video.audio +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 @@ -14,6 +15,7 @@ import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint import app.revanced.util.findMethodFromToString import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.insertLiteralOverride @@ -55,10 +57,12 @@ val forceOriginalAudioPatch = bytecodePatch( addResources("youtube", "video.audio.forceOriginalAudioPatch") PreferenceScreen.VIDEO.addPreferences( - SwitchPreference( - key = "revanced_force_original_audio", - tag = "app.revanced.extension.youtube.settings.preference.ForceOriginalAudioSwitchPreference" - ) + SwitchPreference("revanced_force_original_audio") + ) + + mainActivityOnCreateFingerprint.method.addInstruction( + 0, + "invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setPreferredLanguage()V" ) // Disable feature flag that ignores the default track flag diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 197680376..ac8860273 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -125,14 +125,12 @@ - Android TV Android VR - iOS TV + VisionOS - ANDROID_UNPLUGGED ANDROID_VR_1_61_48 - IOS_UNPLUGGED + VISIONOS diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index e1df14c83..a297dd20e 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -765,7 +765,7 @@ If changing this setting does not take effect, try switching to Incognito mode." "Audio track menu is hidden -To show the Audio track menu, change \'Spoof video streams\' to iOS TV" +Audio track menu is not available when \'Spoof video streams\' is enabled" Hide Watch in VR Watch in VR menu is hidden @@ -1613,32 +1613,25 @@ Enabling this can unlock higher video qualities" Spoof video streams Spoof the client video streams to prevent playback issues Spoof video streams - Video streams are spoofed + "Video streams are spoofed + +If you are a YouTube Premium user, this setting may not be required" "Video streams are not spoofed Video playback may not work" Turning off this setting may cause video playback issues. Default client - Force iOS AVC (H.264) - Video codec is forced to AVC (H.264) - Video codec is determined automatically - "Enabling this might improve battery life and fix playback stuttering. - -AVC has a maximum resolution of 1080p, Opus audio codec is not available, and video playback will use more internet data than VP9 or AV1." - iOS spoofing side effects - "• Movies or paid videos may not play -• Stable volume is not available -• Videos end 1 second early" + Spoofing side effects Android spoofing side effects "• Audio track menu is missing -• Stable volume is not available -• Force original audio is not available" +• Stable volume is not available" + • Experimental client and may stop working anytime • No AV1 video codec • Kids videos may not play when logged out or in incognito mode Show in Stats for nerds Client type is shown in Stats for nerds Client is hidden in Stats for nerds - VR default audio stream language + Audio stream language