From 75e84a28cda70b24b9ee5227e2b4b862063c241a Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Sun, 12 May 2024 11:55:17 +0900 Subject: [PATCH] fix(YouTube/Spoof format stream data): check audio tags first --- .../misc/SpoofFormatStreamDataPatch.java | 95 ++++++++++++++++--- 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofFormatStreamDataPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofFormatStreamDataPatch.java index 6ac9bba12e..20361898f7 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofFormatStreamDataPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/SpoofFormatStreamDataPatch.java @@ -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+. @@ -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 + * YouTube Formats + * 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. */ @@ -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); @@ -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(); @@ -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); @@ -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);