Skip to content

Commit

Permalink
fix(YouTube - Spoof video streams): Add iOS TV client, restore iOS 'f…
Browse files Browse the repository at this point in the history
…orce AVC', show client type in stats for nerds (#4202)
  • Loading branch information
LisoUseInAIKyrios authored Dec 23, 2024
1 parent fea8cab commit ab29f80
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent;
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability;

import app.revanced.extension.shared.spoof.AudioStreamLanguage;
import app.revanced.extension.shared.spoof.ClientType;

/**
* Settings shared across multiple apps.
Expand All @@ -20,5 +23,11 @@ public class BaseSettings {
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);

public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
public static final EnumSetting<AudioStreamLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AudioStreamLanguage.DEFAULT, parent(SPOOF_VIDEO_STREAMS));
public static final EnumSetting<AudioStreamLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AudioStreamLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability());
// Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS));

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

import java.util.Locale;

import app.revanced.extension.shared.Utils;

public enum AudioStreamLanguage {
/**
* YouTube default.
* Can be the original language or can be app language,
* depending on what YouTube decides to pick as the default.
* The current app language.
*/
DEFAULT,

// Language codes found in locale_config.xml
// Region specific variants of Chinese/English/Spanish/French have been removed.
// All region specific variants have been removed.
AF,
AM,
AR,
Expand Down Expand Up @@ -67,6 +63,7 @@ public enum AudioStreamLanguage {
OR,
PA,
PL,
PT,
RO,
RU,
SI,
Expand Down Expand Up @@ -94,6 +91,9 @@ public enum AudioStreamLanguage {
language = name().toLowerCase(Locale.US);
}

/**
* @return The 2 letter ISO 639_1 language code.
*/
public String getLanguage() {
// Changing the app language does not force the app to completely restart,
// so the default needs to be the current language and not a static field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,21 @@

import androidx.annotation.Nullable;

import app.revanced.extension.shared.settings.BaseSettings;

public enum ClientType {
// https://dumps.tadiphone.dev/dumps/oculus/eureka
ANDROID_VR_NO_AUTH( // Must be first so a default audio language can be set.
ANDROID_VR_NO_AUTH(
28,
"ANDROID_VR",
"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",
false),
// Fall over to authenticated ('hl' is ignored and audio is same as language set in users Google account).
ANDROID_VR(
ANDROID_VR_NO_AUTH.id,
ANDROID_VR_NO_AUTH.clientName,
ANDROID_VR_NO_AUTH.deviceModel,
ANDROID_VR_NO_AUTH.osVersion,
ANDROID_VR_NO_AUTH.userAgent,
ANDROID_VR_NO_AUTH.androidSdkVersion,
ANDROID_VR_NO_AUTH.clientVersion,
true),
false,
"Android VR No auth"
),
ANDROID_UNPLUGGED(
29,
"ANDROID_UNPLUGGED",
Expand All @@ -33,7 +27,49 @@ public enum ClientType {
"com.google.android.apps.youtube.unplugged/8.49.0 (Linux; U; Android 14; GB) gzip",
"34",
"8.49.0",
true); // Requires login.
true,
"Android TV"
),
ANDROID_VR(
ANDROID_VR_NO_AUTH.id,
ANDROID_VR_NO_AUTH.clientName,
ANDROID_VR_NO_AUTH.deviceModel,
ANDROID_VR_NO_AUTH.osVersion,
ANDROID_VR_NO_AUTH.userAgent,
ANDROID_VR_NO_AUTH.androidSdkVersion,
ANDROID_VR_NO_AUTH.clientVersion,
true,
"Android VR"
),
IOS_UNPLUGGED(33,
"IOS_UNPLUGGED",
forceAVC()
? "iPhone12,5" // 11 Pro Max (last device with iOS 13)
: "iPhone16,2", // 15 Pro Max
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
forceAVC()
? "13.7.17H35" // Last release of iOS 13.
: "18.1.1.22B91",
forceAVC()
? "com.google.ios.youtubeunplugged/6.45 (iPhone; U; CPU iOS 13_7 like Mac OS X)"
: "com.google.ios.youtubeunplugged/8.33 (iPhone; U; CPU iOS 18_1_1 like Mac OS X)",
null,
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/152043/
// Some newer versions can also force AVC,
// but 6.45 is the last version that supports iOS 13.
forceAVC()
? "6.45"
: "8.33",
true,
forceAVC()
? "iOS TV Force AVC"
: "iOS TV"
);

private static boolean forceAVC() {
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
}

/**
* YouTube
Expand Down Expand Up @@ -75,14 +111,20 @@ public enum ClientType {
*/
public final boolean canLogin;

/**
* Friendly name displayed in stats for nerds.
*/
public final String friendlyName;

ClientType(int id,
String clientName,
String deviceModel,
String osVersion,
String userAgent,
@Nullable String androidSdkVersion,
String clientVersion,
boolean canLogin) {
boolean canLogin,
String friendlyName) {
this.id = id;
this.clientName = clientName;
this.deviceModel = deviceModel;
Expand All @@ -91,5 +133,7 @@ public enum ClientType {
this.androidSdkVersion = androidSdkVersion;
this.clientVersion = clientVersion;
this.canLogin = canLogin;
this.friendlyName = friendlyName;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.revanced.extension.shared.spoof;

import android.net.Uri;
import android.text.TextUtils;

import androidx.annotation.Nullable;

Expand All @@ -17,6 +18,9 @@
public class SpoofVideoStreamsPatch {
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();

private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;

/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
Expand All @@ -30,17 +34,6 @@ private static boolean isPatchIncluded() {
return false; // Modified during patching.
}

public static final class NotSpoofingAndroidAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
if (SpoofVideoStreamsPatch.isPatchIncluded()) {
return !BaseSettings.SPOOF_VIDEO_STREAMS.get();
}

return true;
}
}

/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
Expand Down Expand Up @@ -97,6 +90,17 @@ public static boolean isSpoofingEnabled() {
return SPOOF_STREAMING_DATA;
}

/**
* Injection point.
* Only invoked when playing a livestream on an iOS client.
*/
public static boolean fixHLSCurrentTime(boolean original) {
if (!SPOOF_STREAMING_DATA) {
return original;
}
return false;
}

/**
* Injection point.
*/
Expand Down Expand Up @@ -183,4 +187,50 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos

return postData;
}

/**
* Injection point.
*/
public static String appendSpoofedClient(String videoFormat) {
try {
if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
&& !TextUtils.isEmpty(videoFormat)) {
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages.
return "\u202D" + videoFormat + "\u2009(" // u202D = left to right override
+ StreamingDataRequest.getLastSpoofedClientName() + ")";
}
} catch (Exception ex) {
Logger.printException(() -> "appendSpoofedClient failure", ex);
}

return videoFormat;
}

public static final class NotSpoofingAndroidAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
if (SpoofVideoStreamsPatch.isPatchIncluded()) {
return !BaseSettings.SPOOF_VIDEO_STREAMS.get()
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
}

return true;
}
}

public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return !BaseSettings.SPOOF_VIDEO_STREAMS.get()
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_VR_NO_AUTH;
}
}

public static final class SpoofiOSAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.spoof.AudioStreamLanguage;
import app.revanced.extension.shared.spoof.ClientType;

final class PlayerRoutes {
Expand All @@ -36,8 +37,17 @@ static String createInnertubeBody(ClientType clientType) {
try {
JSONObject context = new JSONObject();

// Can override default language only if no login is used.
// Could use preferred audio for all clients that do not login,
// but if this is a fall over client it will set the language even though
// the audio language is not selectable in the UI.
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
AudioStreamLanguage language = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get()
: AudioStreamLanguage.DEFAULT;

JSONObject client = new JSONObject();
client.put("hl", BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLanguage());
client.put("hl", language.getLanguage());
client.put("clientName", clientType.clientName);
client.put("clientVersion", clientType.clientVersion);
client.put("deviceModel", clientType.deviceModel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.spoof.AudioStreamLanguage;
import app.revanced.extension.shared.spoof.ClientType;

/**
Expand All @@ -36,7 +35,22 @@
*/
public class StreamingDataRequest {

private static final ClientType[] CLIENT_ORDER_TO_USE = ClientType.values();
private static final ClientType[] CLIENT_ORDER_TO_USE;

static {
ClientType[] allClientTypes = ClientType.values();
ClientType preferredClient = BaseSettings.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";

Expand Down Expand Up @@ -73,6 +87,13 @@ protected boolean removeEldestEntry(Entry eldest) {
}
});

private static volatile ClientType lastSpoofedClientType;

public static String getLastSpoofedClientName() {
ClientType client = lastSpoofedClientType;
return client == null ? "Unknown" : client.friendlyName;
}

private final String videoId;

private final Future<ByteBuffer> future;
Expand Down Expand Up @@ -164,20 +185,14 @@ private static ByteBuffer fetch(String videoId, Map<String, String> playerHeader
// Show an error if the last client type fails, or if the debug is enabled then show for all attempts.
final boolean showErrorToast = (++i == CLIENT_ORDER_TO_USE.length) || debugEnabled;

if (clientType == ClientType.ANDROID_VR_NO_AUTH
&& BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get() == AudioStreamLanguage.DEFAULT) {
// Only use no auth Android VR if a non default audio language is selected.
continue;
}

HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
if (connection != null) {
try {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() == 0) {
if (BaseSettings.DEBUG.get()) {
Logger.printException(() -> "Ignoring empty client response: " + clientType);
Logger.printException(() -> "Ignoring empty client: " + clientType);
}
} else {
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
Expand All @@ -188,6 +203,7 @@ private static ByteBuffer fetch(String videoId, Map<String, String> playerHeader
while ((bytesRead = inputStream.read(buffer)) >= 0) {
baos.write(buffer, 0, bytesRead);
}
lastSpoofedClientType = clientType;

return ByteBuffer.wrap(baos.toByteArray());
}
Expand All @@ -198,7 +214,8 @@ private static ByteBuffer fetch(String videoId, Map<String, String> playerHeader
}
}

handleConnectionError("Could not fetch any client streams", null, debugEnabled);
lastSpoofedClientType = null;
handleConnectionError("Could not fetch any client streams", null, true);
return null;
}

Expand Down
Loading

0 comments on commit ab29f80

Please sign in to comment.