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 ade26a30fb..e668eaf9ed 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 @@ -1,7 +1,7 @@ package app.revanced.extension.music.patches.spoof; import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE; -import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK; +import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL; 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; @@ -19,7 +19,7 @@ public class SpoofVideoStreamsPatch { public static void setClientOrderToUse() { List availableClients = List.of( ANDROID_VR_1_43_32, - ANDROID_NO_SDK, + ANDROID_REEL, VISIONOS, ANDROID_VR_1_61_48 ); 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 39076b562d..71133b481a 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 @@ -9,9 +9,35 @@ import java.util.Locale; import java.util.Objects; import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; @SuppressWarnings("ConstantLocale") public enum ClientType { + /** + * Video not playable: Paid, Movie, Private, Age-restricted. + * Uses non-adaptive bitrate. + * AV1 codec available. + */ + ANDROID_REEL( + 3, + "ANDROID", + "com.google.android.youtube", + Build.MANUFACTURER, + Build.MODEL, + "Android", + Build.VERSION.RELEASE, + String.valueOf(Build.VERSION.SDK_INT), + Build.ID, + "20.26.46", + // This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay. + // This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from Morphe. + // This means that the GVS server can strengthen its validation of the ANDROID_REEL client. + // For this reason, ANDROID_REEL is used as a logout client. + false, + true, + false, + "Android Reel" + ), /** * Video not playable: Kids / Paid / Movie / Private / Age-restricted. * This client can only be used when logged out. @@ -28,10 +54,10 @@ public enum ClientType { // Android 12.1 "32", "SQ3A.220605.009.A1", - "132.0.6808.3", "1.61.48", false, false, + true, "Android VR 1.61" ), /** @@ -48,39 +74,12 @@ public enum ClientType { 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.useAuth, ANDROID_VR_1_61_48.supportsMultiAudioTracks, + ANDROID_VR_1_61_48.usePlayerEndpoint, "Android VR 1.43" ), - /** - * Video not playable: Paid / Movie / Private / Age-restricted. - * Note: The 'Authorization' key must be excluded from the header. - * - * According to TeamNewPipe in 2022, if the 'androidSdkVersion' field is missing, - * the GVS did not return a valid response: - * [NewPipe#8713 (comment)](https://github.com/TeamNewPipe/NewPipe/issues/8713#issuecomment-1207443550). - * - * According to the latest commit in yt-dlp, the GVS returns a valid response - * even if the 'androidSdkVersion' field is missing: - * [yt-dlp#14693](https://github.com/yt-dlp/yt-dlp/pull/14693). - * - * For some reason, PoToken is not required. - */ - ANDROID_NO_SDK( - 3, - "ANDROID", - "", - "", - "", - Build.VERSION.RELEASE, - "20.05.46", - "com.google.android.youtube/20.05.46 (Linux; U; Android " + Build.VERSION.RELEASE + ") gzip", - false, - true, - "Android No SDK" - ), /** * Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children". * Google Pixel 9 Pro Fold @@ -95,10 +94,10 @@ public enum ClientType { "15", "35", "AP3A.241005.015.A2", - "132.0.6779.0", "23.47.101", true, false, + true, "Android Studio" ), /** @@ -114,32 +113,8 @@ public enum ClientType { "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" - ), - /** - * The device machine id for the iPad 6th Gen (iPad7,6). - * AV1 hardware decoding is not supported. - * See [this GitHub Gist](https://gist.github.com/adamawolf/3048717) for more information. - * - * Based on Google's actions to date, PoToken may not be required on devices with very low specs. - * For example, suppose the User-Agent for a PlayStation 3 (with 256MB of RAM) is used. - * Accessing 'Web' (https://www.youtube.com) will redirect to 'TV' (https://www.youtube.com/tv). - * 'TV' target devices with very low specs, such as embedded devices, game consoles, and blu-ray players, so PoToken is not required. - * - * For this reason, the device machine id for the iPad 6th Gen (with 2GB of RAM), - * the lowest spec device capable of running iPadOS 17, was used. - */ - IPADOS(5, - "IOS", - "Apple", - "iPad7,6", - "iPadOS", - "17.7.10.21H450", - "19.22.3", - "com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")", - false, true, - "iPadOS" + "visionOS" ); /** @@ -195,13 +170,6 @@ public enum ClientType { @Nullable private final String buildId; - /** - * Cronet release version, as found in decompiled client apk. - * Field is null if not applicable. - */ - @Nullable - private final String cronetVersion; - /** * App version. */ @@ -217,6 +185,11 @@ public enum ClientType { */ public final boolean supportsMultiAudioTracks; + /** + * If the client should use the player endpoint for stream extraction. + */ + public final boolean usePlayerEndpoint; + /** * Friendly name displayed in stats for nerds. */ @@ -234,10 +207,10 @@ public enum ClientType { String osVersion, @NonNull String androidSdkVersion, @NonNull String buildId, - @NonNull String cronetVersion, String clientVersion, boolean useAuth, boolean supportsMultiAudioTracks, + boolean usePlayerEndpoint, String friendlyName) { this.id = id; this.clientName = clientName; @@ -248,21 +221,20 @@ public enum ClientType { this.osVersion = osVersion; this.androidSdkVersion = androidSdkVersion; this.buildId = buildId; - this.cronetVersion = cronetVersion; this.clientVersion = clientVersion; this.useAuth = useAuth; this.supportsMultiAudioTracks = supportsMultiAudioTracks; + this.usePlayerEndpoint = usePlayerEndpoint; this.friendlyName = friendlyName; Locale defaultLocale = Locale.getDefault(); - this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)", + this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s)", packageName, clientVersion, osVersion, defaultLocale, deviceModel, - Objects.requireNonNull(buildId), - Objects.requireNonNull(cronetVersion) + buildId ); Logger.printDebug(() -> "userAgent: " + this.userAgent); } @@ -278,6 +250,7 @@ public enum ClientType { String userAgent, boolean useAuth, boolean supportsMultiAudioTracks, + boolean usePlayerEndpoint, String friendlyName) { this.id = id; this.clientName = clientName; @@ -289,10 +262,10 @@ public enum ClientType { this.userAgent = userAgent; this.useAuth = useAuth; this.supportsMultiAudioTracks = supportsMultiAudioTracks; + this.usePlayerEndpoint = usePlayerEndpoint; 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/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java index 31e3f03034..959048d1e2 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 @@ -15,13 +15,20 @@ 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( + static final Route.CompiledRoute GET_PLAYER_STREAMING_DATA = new Route( Route.Method.POST, "player" + "?fields=streamingData" + "&alt=proto" ).compile(); + static final Route.CompiledRoute GET_REEL_STREAMING_DATA = new Route( + Route.Method.POST, + "reel/reel_item_watch" + + "?fields=playerResponse.playabilityStatus,playerResponse.streamingData" + + "&alt=proto" + ).compile(); + private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; /** @@ -47,6 +54,7 @@ final class PlayerRoutes { Locale streamLocale = language.getLocale(); JSONObject client = new JSONObject(); + client.put("deviceMake", clientType.deviceMake); client.put("deviceModel", clientType.deviceModel); client.put("clientName", clientType.clientName); @@ -61,9 +69,19 @@ final class PlayerRoutes { context.put("client", client); innerTubeBody.put("context", context); - innerTubeBody.put("contentCheckOk", true); - innerTubeBody.put("racyCheckOk", true); - innerTubeBody.put("videoId", videoId); + + if (clientType.usePlayerEndpoint) { + innerTubeBody.put("contentCheckOk", true); + innerTubeBody.put("racyCheckOk", true); + innerTubeBody.put("videoId", videoId); + } else { + JSONObject playerRequest = new JSONObject(); + playerRequest.put("contentCheckOk", true); + playerRequest.put("racyCheckOk", true); + playerRequest.put("videoId", videoId); + innerTubeBody.put("playerRequest", playerRequest); + innerTubeBody.put("disablePlayerResponse", false); + } } catch (JSONException e) { Logger.printException(() -> "Failed to create innerTubeBody", e); } 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 8eb1eaaab5..4b82b6de3d 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 @@ -1,7 +1,8 @@ package app.revanced.extension.shared.spoof.requests; import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes; -import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; +import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_PLAYER_STREAMING_DATA; +import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_REEL_STREAMING_DATA; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,6 +28,7 @@ import java.util.concurrent.TimeoutException; import app.revanced.extension.shared.ByteTrieSearch; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.requests.Route; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.spoof.ClientType; @@ -146,7 +148,10 @@ public class StreamingDataRequest { final long startTime = System.currentTimeMillis(); try { - HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + Route.CompiledRoute route = clientType.usePlayerEndpoint ? + GET_PLAYER_STREAMING_DATA : GET_REEL_STREAMING_DATA; + + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(route, clientType); connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); 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 2bf442a937..3ec213b30e 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,10 +1,9 @@ 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_NO_SDK; 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.IPADOS; +import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL; import static app.revanced.extension.shared.spoof.ClientType.VISIONOS; import java.util.List; @@ -44,11 +43,11 @@ public class SpoofVideoStreamsPatch { } List availableClients = List.of( - VISIONOS, - ANDROID_CREATOR, ANDROID_VR_1_43_32, - ANDROID_NO_SDK, - IPADOS); + ANDROID_REEL, + VISIONOS, + ANDROID_CREATOR + ); app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse( availableClients, client); 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 86802ee200..fa5e9e1008 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 @@ -88,27 +88,22 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference { + '\n' + str("revanced_spoof_video_streams_about_no_stable_volume") + '\n' + str("revanced_spoof_video_streams_about_no_av1") + '\n' + str("revanced_spoof_video_streams_about_no_force_original_audio"); + case ANDROID_REEL -> + summary = str("revanced_spoof_video_streams_about_playback_failure"); // VR 1.61 is not exposed in the UI and should never be reached here. case ANDROID_VR_1_43_32, ANDROID_VR_1_61_48 -> summary = str("revanced_spoof_video_streams_about_no_audio_tracks") + '\n' + str("revanced_spoof_video_streams_about_no_stable_volume"); - case ANDROID_NO_SDK -> - summary = str("revanced_spoof_video_streams_about_playback_failure"); - case IPADOS -> - summary = str("revanced_spoof_video_streams_about_playback_failure") - + '\n' + str("revanced_spoof_video_streams_about_no_av1"); - case VISIONOS -> + case VISIONOS -> summary = str("revanced_spoof_video_streams_about_experimental") + '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks") + '\n' + str("revanced_spoof_video_streams_about_no_av1"); default -> Logger.printException(() -> "Unknown client: " + clientType); } - // Only iPadOS can play children videos in incognito, but it commonly fails at 1 minute - // or doesn't start playback at all. List the side effect for other clients - // since they will fall over to iPadOS. - if (clientType != ClientType.IPADOS && clientType != ClientType.ANDROID_NO_SDK) { - summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos"); + // Only Android Reel and Android VR supports 360° VR immersive mode. + if (!clientType.name().startsWith("ANDROID_VR") && clientType != ClientType.ANDROID_REEL) { + summary += '\n' + str("revanced_spoof_video_streams_about_no_immersive_mode"); } // Use better formatting for bullet points.