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

Commit

Permalink
fix(YouTube/Spoof format stream data): check audio tags first
Browse files Browse the repository at this point in the history
  • Loading branch information
inotia00 authored and anddea committed May 16, 2024
1 parent ef50cf5 commit 75e84a2
Showing 1 changed file with 81 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import app.revanced.integrations.shared.utils.Logger;
import app.revanced.integrations.shared.utils.Utils;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.VideoInformation;

@SuppressWarnings("unused")
@RequiresApi(26) // Some methods of NewPipeExtractor are only available in Android 8.0+.
Expand All @@ -48,6 +49,27 @@ public final class SpoofFormatStreamDataPatch {
private static final String USER_AGENT
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";

/**
* Itags fallback list
* <a href="https://gist.github.com/MartinEesmaa/2f4b261cb90a47e9c41ba115a011a4aa">YouTube Formats</a>
* TODO: Check if there are any issues with falling back to these itags
*/
private static final int [] AVAILABLE_ITAG_ARRAY = {
251, // Opus
250, // Opus
249, // Opus
313, // VP9, 2160p
271, // VP9, 1440p
248, // VP9, 1080p
247, // VP9, 720p
22, // H.264 (High, L3.1), 720p
136, // H.264, 720p
135, // H.264, 480p
134, // H.264, 360p
133, // H.264, 240p
160, // H.264, 144p
};

/**
* Last video id loaded. Used to prevent reloading the same spec multiple times.
*/
Expand All @@ -63,37 +85,69 @@ public final class SpoofFormatStreamDataPatch {
*/
public static void hookStreamData(Object protobufList) {
try {
if (!spoofFormatStreamData) {
return;
}
if (!(protobufList instanceof List<?> formatsList)) {
return;
}
for (Object formatObject : formatsList) {
Field field = formatObject.getClass().getDeclaredField("replaceMeWithFieldName");
field.setAccessible(true);
if (!(field.get(formatObject) instanceof String url)) continue;

// Set Field
Field urlField = formatObject.getClass().getDeclaredField("replaceMeWithUrlFieldName");
Field itagField = formatObject.getClass().getDeclaredField("replaceMeWithITagFieldName");
Field audioCodecParameterField = formatObject.getClass().getDeclaredField("replaceMeWithAudioCodecParameterFieldName");
audioCodecParameterField.setAccessible(true);
urlField.setAccessible(true);
itagField.setAccessible(true);

// Check Field
if (!(urlField.get(formatObject) instanceof String url)) continue;
if (!(itagField.get(formatObject) instanceof Integer itagInteger)) continue;
if (!(audioCodecParameterField.get(formatObject) instanceof String audioCodecParameter)) continue;

if (!url.contains("googlevideo")) continue;
// Since I used a locally modified NewPipeExtractor - https://github.com/inotia00/NewPipeExtractor -
// it is fetched as ANDROID_TESTSUITE.
// If you use jitpack's NewPipeExtractor library (original), it will be fetched as WEB.
if (url.contains("ANDROID_TESTSUITE")) continue;
var itag = Uri.parse(url).getQueryParameter("itag");
// ANDROID_TESTSUITE does not support live streams.
if (VideoInformation.getLiveStreamState()) continue;

Logger.printDebug(() -> "Original StreamData: " + url);
String itag = Uri.parse(url).getQueryParameter("itag");
if (itag == null) {
Logger.printDebug(() -> "URL does not contain itag: " + url);
continue;
}
Logger.printDebug(() -> "itag field value: " + itagInteger);
if (!audioCodecParameter.isEmpty()) {
Logger.printDebug(() -> "audio codec parameter field value: " + audioCodecParameter);
}

String replacement = formatStreamDataMap.get(Integer.parseInt(itag));
if (replacement == null) {
// lowest quality
Logger.printDebug(() -> "Falling back to itag 133");
replacement = formatStreamDataMap.get(133);
for (int itags : AVAILABLE_ITAG_ARRAY) {
String formatStreamUrl = formatStreamDataMap.get(itags);
if (formatStreamUrl != null) {
Logger.printDebug(() -> "Falling back to itag: " + itags);
replacement = formatStreamUrl;

itagField.set(formatObject, itags);
if (249 <= itags && itags <= 251) {
audioCodecParameterField.set(formatObject, "");
}
break;
}
}
}
if (replacement == null) {
Logger.printDebug(() -> "No replacement found for itag: " + itag);
Logger.printDebug(() -> "No replacement found for itag, ignoring");
continue;
}
String finalReplacement = replacement;
Logger.printDebug(() -> "Original StreamData: " + url);
Logger.printDebug(() -> "Hooked StreamData: " + finalReplacement);
field.set(formatObject, replacement);
urlField.set(formatObject, replacement);
}
} catch (Exception e) {
Logger.printException(() -> "Hooked Error: " + e.getMessage(), e);
Expand Down Expand Up @@ -126,17 +180,33 @@ public static void newPlayerResponseVideoId(@NonNull String videoId, boolean isS

StreamExtractor extractor = new YoutubeService(1).getStreamExtractor(url);
extractor.fetchPage();

StringBuilder sb1 = new StringBuilder("Put audioStream:");
for (AudioStream audioStream : extractor.getAudioStreams()) {
sb1.append(" ");
sb1.append(audioStream.getItag());
sb1.append(",");
formatStreamMap.put(audioStream.getItag(), audioStream.getContent());
}
Logger.printDebug(() -> sb1.toString().replaceFirst(".$", ""));

StringBuilder sb2 = new StringBuilder("Put videoOnlyStream:");
for (VideoStream videoOnlyStream : extractor.getVideoOnlyStreams()) {
sb2.append(" ");
sb2.append(videoOnlyStream.getItag());
sb2.append(",");
formatStreamMap.put(videoOnlyStream.getItag(), videoOnlyStream.getContent());
}
Logger.printDebug(() -> sb2.toString().replaceFirst(".$", ""));

StringBuilder sb3 = new StringBuilder("Put videoStream:");
for (VideoStream videoStream : extractor.getVideoStreams()) {
sb3.append(" ");
sb3.append(videoStream.getItag());
sb3.append(",");
formatStreamMap.put(videoStream.getItag(), videoStream.getContent());
}
Logger.printDebug(() -> sb3.toString().replaceFirst(".$", ""));

return formatStreamMap;
}).get();
Expand Down Expand Up @@ -185,8 +255,6 @@ private static void handleConnectionError(@NonNull String toastMessage, @Nullabl
@Nullable
private static HttpURLConnection makeRequest(final Request request) {
try {
Logger.printDebug(() -> "Hooked request");

HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();
connection.setRequestMethod(request.httpMethod());
connection.setUseCaches(false);
Expand All @@ -205,16 +273,15 @@ private static HttpURLConnection makeRequest(final Request request) {
connection.addRequestProperty(headerName, headerValueList.get(0));
}
}
Logger.printDebug(() -> "Hooked headers");

final byte[] innerTubeBody = request.dataToSend();
if (innerTubeBody != null) {
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
}
Logger.printDebug(() -> "Hooked body");

final int responseCode = connection.getResponseCode();
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
Logger.printDebug(() -> "Fetch successed");
return connection;
} else if (responseCode == HTTP_STATUS_CODE_RATE_LIMIT) {
handleConnectionError("Hooked reCaptcha Challenge requested", null);
Expand Down

0 comments on commit 75e84a2

Please sign in to comment.