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): incorrect url is used
Browse files Browse the repository at this point in the history
  • Loading branch information
inotia00 authored and anddea committed May 16, 2024
1 parent 75e84a2 commit 2f86f6b
Showing 1 changed file with 47 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
Expand Down Expand Up @@ -50,31 +51,10 @@ public final class SpoofFormatStreamDataPatch {
= "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.
* Last endpoint video id loaded. Used to prevent reloading the same spec multiple times.
*/
@NonNull
private static volatile String lastPlayerResponseVideoId = "";
private static volatile String lastEndpointVideoId = "";

private static volatile Map<Integer, String> formatStreamDataMap;

Expand All @@ -88,31 +68,28 @@ public static void hookStreamData(Object protobufList) {
if (!spoofFormatStreamData) {
return;
}
if (formatStreamDataMap == null || formatStreamDataMap.isEmpty()) {
return;
}
if (!(protobufList instanceof List<?> formatsList)) {
return;
}
for (Object formatObject : formatsList) {

// 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;
// ANDROID_TESTSUITE does not support live streams.
if (VideoInformation.getLiveStreamState()) continue;

Logger.printDebug(() -> "Original StreamData: " + url);
String itag = Uri.parse(url).getQueryParameter("itag");
Expand All @@ -121,32 +98,13 @@ public static void hookStreamData(Object protobufList) {
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) {
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, ignoring");
continue;
}
String finalReplacement = replacement;
Logger.printDebug(() -> "Hooked StreamData: " + finalReplacement);
Logger.printDebug(() -> "Hooked StreamData: " + replacement);
urlField.set(formatObject, replacement);
}
} catch (Exception e) {
Expand All @@ -155,17 +113,30 @@ public static void hookStreamData(Object protobufList) {
}

/**
* Injection point.
* TODO: Make sure there are no issues without checking if the current video is Shorts.
* PlayerResponse is made after StreamingData is invoked.
* Therefore, we cannot use {@link VideoInformation#getPlayerResponseVideoId}.
* Instead, use the videoId query parameter in EndpointUrl.
*
* @param endpointUrl It has a similar format to the 'baseEndpointUrl' variable in the {@link YoutubeParsingHelper#getMobilePostResponse} method.
*/
public static void newPlayerResponseVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
public static void newEndpointUrlResponse(@Nullable String endpointUrl) {
// Example format for EndpointUrl:
// https://youtubei.googleapis.com/youtubei/v1/player?key=AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w&t=J2aJjSG9n2pQ&id=dQw4w9WgXcQ
String videoId = Uri.parse(endpointUrl).getQueryParameter("id");
if (videoId != null) {
Logger.printDebug(() -> "newEndpointUrlResponse: " + endpointUrl);
setFormatStreamData(videoId);
}
}

private static void setFormatStreamData(@NonNull String videoId) {
if (!spoofFormatStreamData) {
return;
}
if (videoId.equals(lastPlayerResponseVideoId)) {
if (videoId.equals(lastEndpointVideoId)) {
return;
}
lastPlayerResponseVideoId = videoId;
lastEndpointVideoId = videoId;

try {
formatStreamDataMap = Utils.submitOnBackgroundThread(() -> {
Expand All @@ -181,32 +152,32 @@ public static void newPlayerResponseVideoId(@NonNull String videoId, boolean isS
StreamExtractor extractor = new YoutubeService(1).getStreamExtractor(url);
extractor.fetchPage();

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

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

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

return formatStreamMap;
}).get();
Expand All @@ -215,6 +186,13 @@ public static void newPlayerResponseVideoId(@NonNull String videoId, boolean isS
}
}

private static void handleStreamBuilder(StringBuilder sb) {
String message = sb.toString();
if (message.charAt(message.length() - 1) == ',') {
Logger.printDebug(() -> message.replaceFirst(".$", ""));
}
}

private static Downloader getDownloader() {
return new Downloader() {
@Override
Expand Down

0 comments on commit 2f86f6b

Please sign in to comment.