Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(YouTube - Spoof video streams): Log out the iOS client to allow kids videos playback #4000

Merged
merged 6 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
import androidx.annotation.Nullable;

public enum ClientType {
// Specific purpose for age restricted, or private videos, because the iOS client is not logged in.
ANDROID_VR(28,
"Quest 3",
"12",
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
"32", // Android 12.1
"1.56.21",
"ANDROID_VR",
true
),
// Specific for kids videos.
// https://dumps.tadiphone.dev/dumps/oculus/eureka
IOS(5,
// iPhone 15 supports AV1 hardware decoding.
Expand All @@ -25,14 +36,9 @@ public enum ClientType {
null,
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/185230
"19.10.7"
),
ANDROID_VR(28,
"Quest 3",
"12",
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
"32", // Android 12.1
"1.56.21"
"19.10.7",
"IOS",
false
);

/**
Expand All @@ -44,7 +50,7 @@ public enum ClientType {
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String model;
public final String deviceModel;

/**
* Device OS version.
Expand All @@ -63,17 +69,37 @@ public enum ClientType {
@Nullable
public final String androidSdkVersion;

/**
* Client name.
*/
public final String clientName;

/**
* App version.
*/
public final String appVersion;
public final String clientVersion;

/**
* If the client can access the API logged in.
*/
public final boolean canLogin;

ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) {
ClientType(int id,
String deviceModel,
String osVersion,
String userAgent,
@Nullable String androidSdkVersion,
String clientVersion,
String clientName,
boolean canLogin
) {
this.id = id;
this.model = model;
this.deviceModel = deviceModel;
this.osVersion = osVersion;
this.userAgent = userAgent;
this.androidSdkVersion = androidSdkVersion;
this.appVersion = appVersion;
this.clientVersion = clientVersion;
this.clientName = clientName;
this.canLogin = canLogin;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@
import app.revanced.extension.youtube.requests.Route;

final class PlayerRoutes {
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";

static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
Route.Method.POST,
"player" +
"?fields=streamingData" +
"&alt=proto"
).compile();

private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
/**
* TCP connection and HTTP read timeout
*/
Expand All @@ -30,15 +28,15 @@ private PlayerRoutes() {
}

static String createInnertubeBody(ClientType clientType) {
JSONObject innerTubeBody = new JSONObject();
JSONObject innerTubeBody = new JSONObject();

try {
JSONObject context = new JSONObject();

JSONObject client = new JSONObject();
client.put("clientName", clientType.name());
client.put("clientVersion", clientType.appVersion);
client.put("deviceModel", clientType.model);
client.put("clientVersion", clientType.clientVersion);
client.put("deviceModel", clientType.deviceModel);
client.put("osVersion", clientType.osVersion);
if (clientType.androidSdkVersion != null) {
client.put("androidSdkVersion", clientType.androidSdkVersion);
Expand All @@ -57,7 +55,9 @@ static String createInnertubeBody(ClientType clientType) {
return innerTubeBody.toString();
}

/** @noinspection SameParameterValue*/
/**
* @noinspection SameParameterValue
*/
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
/**
* Video streaming data. Fetching is tied to the behavior YT uses,
* where this class fetches the streams only when YT fetches.
*
* <p>
* Effectively the cache expiration of these fetches is the same as the stock app,
* since the stock app would not use expired streams and therefor
* the extension replace stream hook is called only if YT
Expand All @@ -37,38 +37,20 @@
public class StreamingDataRequest {

private static final ClientType[] CLIENT_ORDER_TO_USE;

static {
ClientType[] allClientTypes = ClientType.values();
ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();

CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
CLIENT_ORDER_TO_USE[0] = preferredClient;

int i = 1;
for (ClientType c : allClientTypes) {
if (c != preferredClient) {
CLIENT_ORDER_TO_USE[i++] = c;
}
}
}

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String[] REQUEST_HEADER_KEYS = {
"Authorization", // Available only to logged in users.
AUTHORIZATION_HEADER, // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
};

/**
* TCP connection and HTTP read timeout.
*/
private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;

/**
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
*/
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;

private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
new LinkedHashMap<>(100) {
/**
Expand All @@ -86,8 +68,32 @@ protected boolean removeEldestEntry(Entry eldest) {
}
});

static {
ClientType[] allClientTypes = ClientType.values();
ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();

CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
CLIENT_ORDER_TO_USE[0] = preferredClient;

int i = 1;
for (ClientType c : allClientTypes) {
if (c != preferredClient) {
CLIENT_ORDER_TO_USE[i++] = c;
}
}
}

private final String videoId;
private final Future<ByteBuffer> future;

private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
}

public static void fetchRequest(String videoId, Map<String, String> fetchHeaders) {
// Always fetch, even if there is a existing request for the same video.
// Always fetch, even if there is an existing request for the same video.
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
}

Expand Down Expand Up @@ -119,6 +125,10 @@ private static HttpURLConnection send(ClientType clientType, String videoId,
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);

for (String key : REQUEST_HEADER_KEYS) {
if (!clientType.canLogin && key.equals(AUTHORIZATION_HEADER)) {
continue;
}

String value = playerHeaders.get(key);
if (value != null) {
connection.setRequestProperty(key, value);
Expand Down Expand Up @@ -186,15 +196,6 @@ private static ByteBuffer fetch(String videoId, Map<String, String> playerHeader
return null;
}

private final String videoId;
private final Future<ByteBuffer> future;

private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
}

public boolean fetchCompleted() {
return future.isDone();
}
Expand Down
2 changes: 1 addition & 1 deletion patches/src/main/resources/addresources/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
</patch>
<patch id="chat.autoclaim.autoClaimChannelPointsPatch">
</patch>
<patch id="ad.embedded.embeddedAdsPatch">
<patch id="ad.embedded.embeddedAdsPatch">
<string-array name="revanced_block_embedded_ads_entries">
<item>@string/revanced_block_embedded_ads_entry_1</item>
<item>@string/revanced_block_embedded_ads_entry_2</item>
Expand Down
4 changes: 2 additions & 2 deletions patches/src/main/resources/addresources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1224,9 +1224,9 @@ This is because Crowdin requires temporarily flattening this file and removing t
<string name="revanced_spoof_video_streams_ios_force_avc_no_hardware_vp9_summary_on">Your device does not have VP9 hardware decoding, and this setting is always on when Client spoofing is enabled</string>
<string name="revanced_spoof_video_streams_ios_force_avc_user_dialog_message">Enabling this might improve battery life and fix playback stuttering.\n\nAVC has a maximum resolution of 1080p, and video playback will use more internet data than VP9 or AV1.</string>
<string name="revanced_spoof_video_streams_about_ios_title">iOS spoofing side effects</string>
<string name="revanced_spoof_video_streams_about_ios_summary">• Movies or paid videos may not play\n• Livestreams start from the beginning\n• Videos may end 1 second early\n• No opus audio codec</string>
<string name="revanced_spoof_video_streams_about_ios_summary">• Private kids videos will not play\n• Livestreams start from the beginning\n• Videos may end 1 second early\n• No opus audio codec</string>
<string name="revanced_spoof_video_streams_about_android_vr_title">Android VR spoofing side effects</string>
<string name="revanced_spoof_video_streams_about_android_vr_summary">• Audio track menu is missing\n• Stable volume is not available</string>
<string name="revanced_spoof_video_streams_about_android_vr_summary">• Kids videos may not play\n• Audio track menu is missing\n• Stable volume is not available</string>
</patch>
</app>
<app id="twitch">
Expand Down