From 07b3d8ac3c0f3b774e4ad2126c6d820faf40939c Mon Sep 17 00:00:00 2001 From: Aaron Veil <70171475+anddea@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:12:45 +0300 Subject: [PATCH] feat(YouTube - Spoof streaming data): Add `iOS Compatibility mode` setting --- .../patches/misc/requests/PlayerRoutes.java | 7 ++ .../misc/requests/PlaylistRequest.java | 2 +- .../misc/requests/StreamingDataRequest.java | 67 ++++++++++++++++++- .../youtube/settings/Settings.java | 4 +- ...oofStreamingDataSideEffectsPreference.java | 6 +- 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java index 6d71c49274..289f478542 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java @@ -30,6 +30,13 @@ public final class PlayerRoutes { "?fields=contents.singleColumnWatchNextResults.playlist.playlist" ).compile(); + static final Route.CompiledRoute GET_LIVE_STREAM_RENDERER = new Route( + Route.Method.POST, + "player" + + "?fields=playabilityStatus.status," + + "videoDetails.isLiveContent" + ).compile(); + /** * TCP connection and HTTP read timeout */ diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java index be781f9501..42b3f0905b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java @@ -77,7 +77,7 @@ private static JSONObject send(ClientType clientType, String videoId) { final long startTime = System.currentTimeMillis(); String clientTypeName = clientType.name(); - Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientType.name()); + Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientTypeName); try { HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java index df9839e4c8..fb848fac8b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java @@ -1,11 +1,15 @@ package app.revanced.integrations.youtube.patches.misc.requests; +import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_LIVE_STREAM_RENDERER; import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -23,16 +27,19 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import app.revanced.integrations.shared.requests.Requester; import app.revanced.integrations.shared.utils.Logger; import app.revanced.integrations.shared.utils.Utils; import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType; import app.revanced.integrations.youtube.settings.Settings; public class StreamingDataRequest { + private static final boolean SPOOF_STREAMING_DATA_IOS_COMPATIBILITY = Settings.SPOOF_STREAMING_DATA_IOS_COMPATIBILITY.get(); + private static final ClientType[] allClientTypes = { + ClientType.IOS, ClientType.ANDROID_VR, ClientType.ANDROID_UNPLUGGED, - ClientType.IOS, }; private static final ClientType[] clientTypesToUse; @@ -100,6 +107,59 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti Logger.printInfo(() -> toastMessage, ex); } + private static boolean isUnplayableOrLiveStream(ClientType clientType, String videoId) { + if (!SPOOF_STREAMING_DATA_IOS_COMPATIBILITY || clientType != ClientType.IOS) { + return false; + } + Objects.requireNonNull(videoId); + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_LIVE_STREAM_RENDERER, clientType); + String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) { + JSONObject playerResponse = Requester.parseJSONObject(connection); + final boolean isPlayabilityOk = isPlayabilityStatusOk(playerResponse); + final boolean isLiveStream = isLiveStream(playerResponse); + return !isPlayabilityOk || isLiveStream; + } + + // Always show a toast for this, as a non 200 response means something is broken. + handleConnectionError("Fetch livestreams not available: " + responseCode, null); + } catch (SocketTimeoutException ex) { + handleConnectionError("Fetch livestreams temporarily not available (API timed out)", ex); + } catch (IOException ex) { + handleConnectionError("Fetch livestreams temporarily not available: " + ex.getMessage(), ex); + } catch (Exception ex) { + Logger.printException(() -> "Fetch livestreams failed", ex); // Should never happen. + } + + return true; + } + + private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) { + try { + return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); + } catch (JSONException e) { + Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); + } + + return false; + } + + private static boolean isLiveStream(@NonNull JSONObject playerResponse) { + try { + return playerResponse.getJSONObject("videoDetails").getBoolean("isLiveContent"); + } catch (JSONException e) { + Logger.printDebug(() -> "Failed to get videoDetails for response: " + playerResponse); + } + + return false; + } + private static final String[] REQUEST_HEADER_KEYS = { "Authorization", // Available only to logged in users. "X-GOOG-API-FORMAT-VERSION", @@ -158,6 +218,11 @@ private static ByteBuffer fetch(@NonNull String videoId, Map pla // Retry with different client if empty response body is received. for (ClientType clientType : clientTypesToUse) { + if (isUnplayableOrLiveStream(clientType, videoId)) { + Logger.printDebug(() -> "Ignore IOS spoofing as it is unplayable or a live stream (video: " + videoId + ")"); + continue; + } + HttpURLConnection connection = send(clientType, videoId, playerHeaders); if (connection != null) { try { 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 e04188da38..5f13957ead 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 @@ -543,9 +543,11 @@ public class Settings extends BaseSettings { public static final EnumSetting WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE); // PreferenceScreen: Miscellaneous - Spoof streaming data - public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", FALSE, true, "revanced_spoof_streaming_data_user_dialog_message"); + // The order of the settings should not be changed otherwise the app may crash + public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message"); public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true, "revanced_spoof_streaming_data_ios_force_avc_user_dialog_message", new SpoofStreamingDataPatch.ForceiOSAVCAvailability()); + public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_COMPATIBILITY = new BooleanSetting("revanced_spoof_streaming_data_ios_compatibility", TRUE, true, new SpoofStreamingDataPatch.ForceiOSAVCAvailability()); public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.IOS, true, parent(SPOOF_STREAMING_DATA)); public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_STREAMING_DATA)); diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java index 58567071e0..db5061b80a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java @@ -76,7 +76,11 @@ private void updateUI() { final String summaryTextKey; if (selectableClientTypes.contains(clientType)) { - summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase(); + if (clientType == ClientType.IOS && Settings.SPOOF_STREAMING_DATA_IOS_COMPATIBILITY.get()) { + summaryTextKey = "revanced_spoof_streaming_data_side_effects_ios_compatibility"; + } else { + summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase(); + } } else { summaryTextKey = "revanced_spoof_streaming_data_side_effects_unknown"; }