Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

Commit

Permalink
feat(YouTube - Spoof streaming data): Add iOS Compatibility mode se…
Browse files Browse the repository at this point in the history
…tting
  • Loading branch information
anddea committed Nov 10, 2024
1 parent 79d34e4 commit 07b3d8a
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -158,6 +218,11 @@ private static ByteBuffer fetch(@NonNull String videoId, Map<String, String> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,9 +543,11 @@ public class Settings extends BaseSettings {
public static final EnumSetting<WatchHistoryType> 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<ClientType> 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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down

0 comments on commit 07b3d8a

Please sign in to comment.