mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-19 08:17:33 +01:00
fix(YouTube - Client spoof): Spoof client to fix playback (#637)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
parent
ea184d050e
commit
4c1f82aa22
@ -81,7 +81,7 @@ public final class VideoInformation {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String newPlayerResponseSignature(@NonNull String signature, boolean isShortAndOpeningOrPlaying) {
|
public static String newPlayerResponseSignature(@NonNull String signature, String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||||
final boolean isShort = playerParametersAreShort(signature);
|
final boolean isShort = playerParametersAreShort(signature);
|
||||||
playerResponseVideoIdIsShort = isShort;
|
playerResponseVideoIdIsShort = isShort;
|
||||||
if (!isShort || isShortAndOpeningOrPlaying) {
|
if (!isShort || isShortAndOpeningOrPlaying) {
|
||||||
|
@ -0,0 +1,264 @@
|
|||||||
|
package app.revanced.integrations.youtube.patches.spoof;
|
||||||
|
|
||||||
|
import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import app.revanced.integrations.shared.Logger;
|
||||||
|
import app.revanced.integrations.shared.Utils;
|
||||||
|
import app.revanced.integrations.youtube.patches.VideoInformation;
|
||||||
|
import app.revanced.integrations.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class SpoofClientPatch {
|
||||||
|
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
|
||||||
|
private static final boolean SPOOF_CLIENT_USE_IOS = Settings.SPOOF_CLIENT_USE_IOS.get();
|
||||||
|
private static final boolean SPOOF_CLIENT_STORYBOARD = SPOOF_CLIENT_ENABLED && !SPOOF_CLIENT_USE_IOS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any unreachable ip address. Used to intentionally fail requests.
|
||||||
|
*/
|
||||||
|
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
|
||||||
|
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static volatile Future<StoryboardRenderer> lastStoryboardFetched;
|
||||||
|
|
||||||
|
private static final Map<String, Future<StoryboardRenderer>> storyboardCache =
|
||||||
|
Collections.synchronizedMap(new LinkedHashMap<>(100) {
|
||||||
|
private static final int CACHE_LIMIT = 100;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Entry eldest) {
|
||||||
|
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* Blocks /get_watch requests by returning a localhost URI.
|
||||||
|
*
|
||||||
|
* @param playerRequestUri The URI of the player request.
|
||||||
|
* @return Localhost URI if the request is a /get_watch request, otherwise the original URI.
|
||||||
|
*/
|
||||||
|
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
|
||||||
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
|
try {
|
||||||
|
String path = playerRequestUri.getPath();
|
||||||
|
|
||||||
|
if (path != null && path.contains("get_watch")) {
|
||||||
|
Logger.printDebug(() -> "Blocking: " + playerRequestUri + " by returning: " + UNREACHABLE_HOST_URI_STRING);
|
||||||
|
|
||||||
|
return UNREACHABLE_HOST_URI;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "blockGetWatchRequest failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerRequestUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* Blocks /initplayback requests.
|
||||||
|
* For iOS, an unreachable host URL can be used, but for Android Testsuite, this is not possible.
|
||||||
|
*/
|
||||||
|
public static String blockInitPlaybackRequest(String originalUrlString) {
|
||||||
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
|
try {
|
||||||
|
var originalUri = Uri.parse(originalUrlString);
|
||||||
|
String path = originalUri.getPath();
|
||||||
|
|
||||||
|
if (path != null && path.contains("initplayback")) {
|
||||||
|
String replacementUriString = (getSpoofClientType() == ClientType.IOS)
|
||||||
|
? UNREACHABLE_HOST_URI_STRING
|
||||||
|
// TODO: Ideally, a local proxy could be setup and block
|
||||||
|
// the request the same way as Burp Suite is capable of
|
||||||
|
// because that way the request is never sent to YouTube unnecessarily.
|
||||||
|
// Just using localhost unfortunately does not work.
|
||||||
|
: originalUri.buildUpon().clearQuery().build().toString();
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Blocking: " + originalUrlString + " by returning: " + replacementUriString);
|
||||||
|
|
||||||
|
return replacementUriString;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalUrlString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClientType getSpoofClientType() {
|
||||||
|
if (SPOOF_CLIENT_USE_IOS) {
|
||||||
|
return ClientType.IOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
StoryboardRenderer renderer = getRenderer(false);
|
||||||
|
if (renderer == null) {
|
||||||
|
// Video is private or otherwise not available.
|
||||||
|
// Test client still works for video playback, but seekbar thumbnails are not available.
|
||||||
|
// Use iOS client instead.
|
||||||
|
Logger.printDebug(() -> "Using iOS client for paid or otherwise restricted video");
|
||||||
|
return ClientType.IOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderer.isLiveStream) {
|
||||||
|
// Test client does not support live streams.
|
||||||
|
// Use the storyboard renderer information to fallback to iOS if a live stream is opened.
|
||||||
|
Logger.printDebug(() -> "Using iOS client for livestream: " + renderer.videoId);
|
||||||
|
return ClientType.IOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClientType.ANDROID_TESTSUITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static int getClientTypeId(int originalClientTypeId) {
|
||||||
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
|
return getSpoofClientType().id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalClientTypeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String getClientVersion(String originalClientVersion) {
|
||||||
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
|
return getSpoofClientType().version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalClientVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean isClientSpoofingEnabled() {
|
||||||
|
return SPOOF_CLIENT_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Storyboard.
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static String setPlayerResponseVideoId(String parameters, String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||||
|
if (SPOOF_CLIENT_STORYBOARD) {
|
||||||
|
try {
|
||||||
|
// VideoInformation is not a dependent patch, and only this single helper method is used.
|
||||||
|
// Hook can be called when scrolling thru the feed and a Shorts shelf is present.
|
||||||
|
// Ignore these videos.
|
||||||
|
if (!isShortAndOpeningOrPlaying && VideoInformation.playerParametersAreShort(parameters)) {
|
||||||
|
Logger.printDebug(() -> "Ignoring Short: " + videoId);
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StoryboardRenderer> storyboard = storyboardCache.get(videoId);
|
||||||
|
if (storyboard == null) {
|
||||||
|
storyboard = Utils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
|
||||||
|
storyboardCache.put(videoId, storyboard);
|
||||||
|
lastStoryboardFetched = storyboard;
|
||||||
|
|
||||||
|
// Block until the renderer fetch completes.
|
||||||
|
// This is desired because if this returns without finishing the fetch
|
||||||
|
// then video will start playback but the storyboard is not ready yet.
|
||||||
|
getRenderer(true);
|
||||||
|
} else {
|
||||||
|
lastStoryboardFetched = storyboard;
|
||||||
|
// No need to block on the fetch since it previously loaded.
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setPlayerResponseVideoId failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters; // Return the original value since we are observing and not modifying.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static StoryboardRenderer getRenderer(boolean waitForCompletion) {
|
||||||
|
var future = lastStoryboardFetched;
|
||||||
|
if (future != null) {
|
||||||
|
try {
|
||||||
|
if (waitForCompletion || future.isDone()) {
|
||||||
|
return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
|
||||||
|
} // else, return null.
|
||||||
|
} catch (TimeoutException ex) {
|
||||||
|
Logger.printDebug(() -> "Could not get renderer (get timed out)");
|
||||||
|
} catch (ExecutionException | InterruptedException ex) {
|
||||||
|
// Should never happen.
|
||||||
|
Logger.printException(() -> "Could not get renderer", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* Called from background threads and from the main thread.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) {
|
||||||
|
if (SPOOF_CLIENT_STORYBOARD) {
|
||||||
|
StoryboardRenderer renderer = getRenderer(false);
|
||||||
|
|
||||||
|
if (renderer != null) {
|
||||||
|
if (!renderer.isLiveStream && renderer.spec != null) {
|
||||||
|
return renderer.spec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalStoryboardRendererSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static int getRecommendedLevel(int originalLevel) {
|
||||||
|
if (SPOOF_CLIENT_STORYBOARD) {
|
||||||
|
StoryboardRenderer renderer = getRenderer(false);
|
||||||
|
|
||||||
|
if (renderer != null) {
|
||||||
|
if (!renderer.isLiveStream && renderer.recommendedLevel != null) {
|
||||||
|
return renderer.recommendedLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ClientType {
|
||||||
|
ANDROID_TESTSUITE(30, "1.9"),
|
||||||
|
IOS(5, Utils.getAppVersionName());
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
final String version;
|
||||||
|
|
||||||
|
ClientType(int id, String version) {
|
||||||
|
this.id = id;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -85,7 +85,7 @@ public class SpoofSignaturePatch {
|
|||||||
*
|
*
|
||||||
* @param parameters Original protobuf parameter value.
|
* @param parameters Original protobuf parameter value.
|
||||||
*/
|
*/
|
||||||
public static String spoofParameter(String parameters, boolean isShortAndOpeningOrPlaying) {
|
public static String spoofParameter(String parameters, String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||||
try {
|
try {
|
||||||
Logger.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
Logger.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
||||||
|
|
||||||
@ -152,12 +152,12 @@ public class SpoofSignaturePatch {
|
|||||||
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
|
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
|
||||||
StoryboardRenderer renderer = getRenderer(false);
|
StoryboardRenderer renderer = getRenderer(false);
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
if (returnNullIfLiveStream && renderer.isLiveStream()) {
|
if (returnNullIfLiveStream && renderer.isLiveStream) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String spec = renderer.getSpec();
|
|
||||||
if (spec != null) {
|
if (renderer.spec != null) {
|
||||||
return spec;
|
return renderer.spec;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,8 +191,9 @@ public class SpoofSignaturePatch {
|
|||||||
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
|
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
|
||||||
StoryboardRenderer renderer = getRenderer(false);
|
StoryboardRenderer renderer = getRenderer(false);
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
Integer recommendedLevel = renderer.getRecommendedLevel();
|
if (renderer.recommendedLevel != null) {
|
||||||
if (recommendedLevel != null) return recommendedLevel;
|
return renderer.recommendedLevel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +215,7 @@ public class SpoofSignaturePatch {
|
|||||||
// Show empty thumbnails so the seek time and chapters still show up.
|
// Show empty thumbnails so the seek time and chapters still show up.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return renderer.getSpec() != null;
|
return renderer.spec != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,42 +4,30 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
public final class StoryboardRenderer {
|
public final class StoryboardRenderer {
|
||||||
|
public final String videoId;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String spec;
|
public final String spec;
|
||||||
private final boolean isLiveStream;
|
public final boolean isLiveStream;
|
||||||
|
/**
|
||||||
|
* Recommended image quality level, or NULL if no recommendation exists.
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private final Integer recommendedLevel;
|
public final Integer recommendedLevel;
|
||||||
|
|
||||||
public StoryboardRenderer(@Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) {
|
public StoryboardRenderer(String videoId, @Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) {
|
||||||
|
this.videoId = videoId;
|
||||||
this.spec = spec;
|
this.spec = spec;
|
||||||
this.isLiveStream = isLiveStream;
|
this.isLiveStream = isLiveStream;
|
||||||
this.recommendedLevel = recommendedLevel;
|
this.recommendedLevel = recommendedLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getSpec() {
|
|
||||||
return spec;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isLiveStream() {
|
|
||||||
return isLiveStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Recommended image quality level, or NULL if no recommendation exists.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public Integer getRecommendedLevel() {
|
|
||||||
return recommendedLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "StoryboardRenderer{" +
|
return "StoryboardRenderer{" +
|
||||||
"isLiveStream=" + isLiveStream +
|
"videoId=" + videoId +
|
||||||
|
", isLiveStream=" + isLiveStream +
|
||||||
", spec='" + spec + '\'' +
|
", spec='" + spec + '\'' +
|
||||||
", recommendedLevel=" + recommendedLevel +
|
", recommendedLevel=" + recommendedLevel +
|
||||||
'}';
|
'}';
|
||||||
|
@ -10,7 +10,6 @@ import org.json.JSONObject;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
final class PlayerRoutes {
|
final class PlayerRoutes {
|
||||||
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
|
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
|
||||||
static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route(
|
static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route(
|
||||||
@ -27,7 +26,7 @@ final class PlayerRoutes {
|
|||||||
/**
|
/**
|
||||||
* TCP connection and HTTP read timeout
|
* TCP connection and HTTP read timeout
|
||||||
*/
|
*/
|
||||||
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 4 * 1000; // 4 Seconds.
|
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
|
||||||
|
|
||||||
static {
|
static {
|
||||||
JSONObject innerTubeBody = new JSONObject();
|
JSONObject innerTubeBody = new JSONObject();
|
||||||
|
@ -19,17 +19,8 @@ import java.util.Objects;
|
|||||||
import static app.revanced.integrations.shared.StringRef.str;
|
import static app.revanced.integrations.shared.StringRef.str;
|
||||||
import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*;
|
import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*;
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
public class StoryboardRendererRequester {
|
public class StoryboardRendererRequester {
|
||||||
|
|
||||||
/**
|
|
||||||
* For videos that have no storyboard.
|
|
||||||
* Usually for low resolution videos as old as YouTube itself.
|
|
||||||
* Does not include paid videos where the renderer fetch fails.
|
|
||||||
*/
|
|
||||||
private static final StoryboardRenderer emptyStoryboard
|
|
||||||
= new StoryboardRenderer(null, false, null);
|
|
||||||
|
|
||||||
private StoryboardRendererRequester() {
|
private StoryboardRendererRequester() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +60,9 @@ public class StoryboardRendererRequester {
|
|||||||
null, showToastOnIOException || BaseSettings.DEBUG_TOAST_ON_ERROR.get());
|
null, showToastOnIOException || BaseSettings.DEBUG_TOAST_ON_ERROR.get());
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
} catch (SocketTimeoutException ex) {
|
} catch (SocketTimeoutException ex) {
|
||||||
handleConnectionError(str("revanced_spoof_storyboard_timeout"), ex, showToastOnIOException);
|
handleConnectionError(str("revanced_spoof_client_storyboard_timeout"), ex, showToastOnIOException);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
handleConnectionError(str("revanced_spoof_storyboard_io_exception", ex.getMessage()),
|
handleConnectionError(str("revanced_spoof_client_storyboard_io_exception", ex.getMessage()),
|
||||||
ex, showToastOnIOException);
|
ex, showToastOnIOException);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen.
|
Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen.
|
||||||
@ -98,22 +89,23 @@ public class StoryboardRendererRequester {
|
|||||||
* @return StoryboardRenderer or null if playabilityStatus is not OK.
|
* @return StoryboardRenderer or null if playabilityStatus is not OK.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody,
|
private static StoryboardRenderer getStoryboardRendererUsingBody(String videoId,
|
||||||
|
@NonNull String innerTubeBody,
|
||||||
boolean showToastOnIOException) {
|
boolean showToastOnIOException) {
|
||||||
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException);
|
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException);
|
||||||
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
|
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
|
||||||
return getStoryboardRendererUsingResponse(playerResponse);
|
return getStoryboardRendererUsingResponse(videoId, playerResponse);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) {
|
private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull String videoId, @NonNull JSONObject playerResponse) {
|
||||||
try {
|
try {
|
||||||
Logger.printDebug(() -> "Parsing response: " + playerResponse);
|
Logger.printDebug(() -> "Parsing response: " + playerResponse);
|
||||||
if (!playerResponse.has("storyboards")) {
|
if (!playerResponse.has("storyboards")) {
|
||||||
Logger.printDebug(() -> "Using empty storyboard");
|
Logger.printDebug(() -> "Using empty storyboard");
|
||||||
return emptyStoryboard;
|
return new StoryboardRenderer(videoId, null, false, null);
|
||||||
}
|
}
|
||||||
final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
|
final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
|
||||||
final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer");
|
final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer");
|
||||||
@ -123,6 +115,7 @@ public class StoryboardRendererRequester {
|
|||||||
|
|
||||||
final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
|
final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
|
||||||
StoryboardRenderer renderer = new StoryboardRenderer(
|
StoryboardRenderer renderer = new StoryboardRenderer(
|
||||||
|
videoId,
|
||||||
rendererElement.getString("spec"),
|
rendererElement.getString("spec"),
|
||||||
isLiveStream,
|
isLiveStream,
|
||||||
rendererElement.has("recommendedLevel")
|
rendererElement.has("recommendedLevel")
|
||||||
@ -144,11 +137,11 @@ public class StoryboardRendererRequester {
|
|||||||
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
|
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
|
||||||
Objects.requireNonNull(videoId);
|
Objects.requireNonNull(videoId);
|
||||||
|
|
||||||
var renderer = getStoryboardRendererUsingBody(
|
var renderer = getStoryboardRendererUsingBody(videoId,
|
||||||
String.format(ANDROID_INNER_TUBE_BODY, videoId), false);
|
String.format(ANDROID_INNER_TUBE_BODY, videoId), false);
|
||||||
if (renderer == null) {
|
if (renderer == null) {
|
||||||
Logger.printDebug(() -> videoId + " not available using Android client");
|
Logger.printDebug(() -> videoId + " not available using Android client");
|
||||||
renderer = getStoryboardRendererUsingBody(
|
renderer = getStoryboardRendererUsingBody(videoId,
|
||||||
String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true);
|
String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true);
|
||||||
if (renderer == null) {
|
if (renderer == null) {
|
||||||
Logger.printDebug(() -> videoId + " not available using TV embedded client");
|
Logger.printDebug(() -> videoId + " not available using TV embedded client");
|
||||||
|
@ -236,6 +236,8 @@ public class Settings extends BaseSettings {
|
|||||||
"revanced_spoof_device_dimensions_user_dialog_message");
|
"revanced_spoof_device_dimensions_user_dialog_message");
|
||||||
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
||||||
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
|
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
|
||||||
|
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true, "revanced_spoof_client_user_dialog_message");
|
||||||
|
public static final BooleanSetting SPOOF_CLIENT_USE_IOS = new BooleanSetting("revanced_spoof_client_use_ios", FALSE, true, parent(SPOOF_CLIENT));
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", "");
|
public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", "");
|
||||||
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1);
|
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1);
|
||||||
@ -244,7 +246,7 @@ public class Settings extends BaseSettings {
|
|||||||
// Debugging
|
// Debugging
|
||||||
/**
|
/**
|
||||||
* When enabled, share the debug logs with care.
|
* When enabled, share the debug logs with care.
|
||||||
* The buffer contains select user data, including the client ip address and information that could identify the YT account.
|
* The buffer contains select user data, including the client ip address and information that could identify the end user.
|
||||||
*/
|
*/
|
||||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG));
|
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG));
|
||||||
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.google.protos.youtube.api.innertube;
|
||||||
|
|
||||||
|
public class InnertubeContext$ClientInfo {
|
||||||
|
public int r;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user