Skip to content

Commit

Permalink
feat(YouTube): Add Spoof client patch
Browse files Browse the repository at this point in the history
  • Loading branch information
anddea committed May 27, 2024
1 parent d14d77b commit 010f879
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class BaseSettings {
public static final BooleanSetting ENABLE_DEBUG_LOGGING = new BooleanSetting("revanced_enable_debug_logging", FALSE);
/**
* 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 ENABLE_DEBUG_BUFFER_LOGGING = new BooleanSetting("revanced_enable_debug_buffer_logging", FALSE);
public static final BooleanSetting SETTINGS_INITIALIZED = new BooleanSetting("revanced_settings_initialized", FALSE, false, false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package app.revanced.integrations.youtube.patches.misc;

import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;

import app.revanced.integrations.shared.utils.Logger;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.PlayerType;

/**
* @noinspection ALL
*/
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();

/**
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
*
* <p>
* See <a href="https://dumps.tadiphone.dev/dumps/oculus/eureka">this GitLab</a> for more
* information.
* </p>
*/
private static final String ANDROID_VR_DEVICE_MODEL = "Quest 3";

/**
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://www.meta.com/en-us/experiences/2002317119880945/">the App
* Store page of the YouTube app</a>, in the {@code Additional details} section.
* </p>
*/
private static final String ANDROID_VR_YOUTUBE_CLIENT_VERSION = "1.56.21";

/**
* The device machine id for the iPhone 15 Pro Max, used to get 60fps with the iOS client.
*
* <p>
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
* information.
* </p>
*/
private static final String IOS_DEVICE_MODEL = "iPhone16,2";

/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664/">the App
* Store page of the YouTube app</a>, in the {@code What’s New} section.
* </p>
*/
private static final String IOS_YOUTUBE_CLIENT_VERSION = "19.20.2";

/**
* Clips or Shorts Parameters.
*/
private static final String[] CLIPS_OR_SHORTS_PARAMETERS = {
"kAIB", // Clips
"8AEB" // Shorts
};

/**
* iOS client is used for Clips or Shorts.
*/
private static volatile boolean useIOSClient;

/**
* 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);

/**
* 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.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_CLIENT_ENABLED) {
try {
Uri originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();

if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking: " + originalUrlString + " by returning unreachable url");

return UNREACHABLE_HOST_URI_STRING;
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}

return originalUrlString;
}

private static ClientType getSpoofClientType() {
if (SPOOF_CLIENT_USE_IOS || useIOSClient) {
return ClientType.IOS;
}
return ClientType.ANDROID_VR;
}

/**
* 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 String getClientModel(String originalClientModel) {
if (SPOOF_CLIENT_ENABLED) {
return getSpoofClientType().model;
}

return originalClientModel;
}

/**
* Injection point.
*/
public static boolean isClientSpoofingEnabled() {
return SPOOF_CLIENT_ENABLED;
}

/**
* Injection point.
*/
public static String setPlayerResponseVideoId(@NonNull String videoId, @Nullable String parameters, boolean isShortAndOpeningOrPlaying) {
useIOSClient = playerParameterIsClipsOrShorts(parameters);

return parameters; // Return the original value since we are observing and not modifying.
}

/**
* @return If the player parameters are for a Short or Clips.
*/
private static boolean playerParameterIsClipsOrShorts(@Nullable String playerParameter) {
if (PlayerType.getCurrent().isNoneOrHidden()) {
return true;
}

return playerParameter != null && StringUtils.startsWithAny(playerParameter, CLIPS_OR_SHORTS_PARAMETERS);
}

private enum ClientType {
ANDROID_VR(28, ANDROID_VR_DEVICE_MODEL, ANDROID_VR_YOUTUBE_CLIENT_VERSION),
IOS(5, IOS_DEVICE_MODEL, IOS_YOUTUBE_CLIENT_VERSION);

final int id;
final String model;
final String version;

ClientType(int id, String model, String version) {
this.id = id;
this.model = model;
this.version = version;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ private static String getStoryboardRendererSpec(String originalStoryboardRendere
if (spoofParameter && !useOriginalStoryboardRenderer) {
final StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
if (returnNullIfLiveStream && renderer.isLiveStream()) {
if (returnNullIfLiveStream && renderer.isLiveStream) {
return null;
}
String spec = renderer.getSpec();
String spec = renderer.spec;
if (spec != null) {
return spec;
}
Expand Down Expand Up @@ -192,7 +192,7 @@ public static int getRecommendedLevel(int originalLevel) {
if (spoofParameter && !useOriginalStoryboardRenderer) {
final StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
Integer recommendedLevel = renderer.getRecommendedLevel();
Integer recommendedLevel = renderer.recommendedLevel;
if (recommendedLevel != null) return recommendedLevel;
}
}
Expand All @@ -215,6 +215,6 @@ public static boolean getSeekbarThumbnailOverrideValue() {
// Show empty thumbnails so the seek time and chapters still show up.
return true;
}
return renderer.getSpec() != null;
return renderer.spec != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,31 @@
* @noinspection ALL
*/
public final class StoryboardRenderer {
public final String videoId;
@Nullable
private final String spec;
private final boolean isLiveStream;
public final String spec;
public final boolean isLiveStream;
/**
* Recommended image quality level, or NULL if no recommendation exists.
*/
@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.isLiveStream = isLiveStream;
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
@Override
public String toString() {
StringBuilder sb = new StringBuilder("StoryboardRenderer{" +
"spec='" + spec);
if (!isLiveStream) {
sb.append('\'' + ", recommendedLevel=").append(recommendedLevel);
}
return sb.append('}').toString();
return "StoryboardRenderer{" +
"videoId=" + videoId +
", isLiveStream=" + isLiveStream +
", spec='" + spec + '\'' +
", recommendedLevel=" + recommendedLevel +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public final class PlayerRoutes {
/**
* 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 {
JSONObject innerTubeBody = new JSONObject();
Expand Down
Loading

0 comments on commit 010f879

Please sign in to comment.