Skip to content

Commit ab29f80

Browse files
fix(YouTube - Spoof video streams): Add iOS TV client, restore iOS 'force AVC', show client type in stats for nerds (ReVanced#4202)
1 parent fea8cab commit ab29f80

File tree

13 files changed

+353
-49
lines changed

13 files changed

+353
-49
lines changed

extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import static java.lang.Boolean.FALSE;
44
import static java.lang.Boolean.TRUE;
55
import static app.revanced.extension.shared.settings.Setting.parent;
6+
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
7+
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability;
68

79
import app.revanced.extension.shared.spoof.AudioStreamLanguage;
10+
import app.revanced.extension.shared.spoof.ClientType;
811

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

2225
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
23-
public static final EnumSetting<AudioStreamLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AudioStreamLanguage.DEFAULT, parent(SPOOF_VIDEO_STREAMS));
26+
public static final EnumSetting<AudioStreamLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AudioStreamLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
27+
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
28+
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
29+
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability());
30+
// Client type must be last spoof setting due to cyclic references.
31+
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));
32+
2433
}

extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/AudioStreamLanguage.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,14 @@
22

33
import java.util.Locale;
44

5-
import app.revanced.extension.shared.Utils;
6-
75
public enum AudioStreamLanguage {
86
/**
9-
* YouTube default.
10-
* Can be the original language or can be app language,
11-
* depending on what YouTube decides to pick as the default.
7+
* The current app language.
128
*/
139
DEFAULT,
1410

1511
// Language codes found in locale_config.xml
16-
// Region specific variants of Chinese/English/Spanish/French have been removed.
12+
// All region specific variants have been removed.
1713
AF,
1814
AM,
1915
AR,
@@ -67,6 +63,7 @@ public enum AudioStreamLanguage {
6763
OR,
6864
PA,
6965
PL,
66+
PT,
7067
RO,
7168
RU,
7269
SI,
@@ -94,6 +91,9 @@ public enum AudioStreamLanguage {
9491
language = name().toLowerCase(Locale.US);
9592
}
9693

94+
/**
95+
* @return The 2 letter ISO 639_1 language code.
96+
*/
9797
public String getLanguage() {
9898
// Changing the app language does not force the app to completely restart,
9999
// so the default needs to be the current language and not a static field.

extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java

+58-14
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,21 @@
44

55
import androidx.annotation.Nullable;
66

7+
import app.revanced.extension.shared.settings.BaseSettings;
8+
79
public enum ClientType {
810
// https://dumps.tadiphone.dev/dumps/oculus/eureka
9-
ANDROID_VR_NO_AUTH( // Must be first so a default audio language can be set.
11+
ANDROID_VR_NO_AUTH(
1012
28,
1113
"ANDROID_VR",
1214
"Quest 3",
1315
"12",
1416
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
1517
"32", // Android 12.1
1618
"1.56.21",
17-
false),
18-
// Fall over to authenticated ('hl' is ignored and audio is same as language set in users Google account).
19-
ANDROID_VR(
20-
ANDROID_VR_NO_AUTH.id,
21-
ANDROID_VR_NO_AUTH.clientName,
22-
ANDROID_VR_NO_AUTH.deviceModel,
23-
ANDROID_VR_NO_AUTH.osVersion,
24-
ANDROID_VR_NO_AUTH.userAgent,
25-
ANDROID_VR_NO_AUTH.androidSdkVersion,
26-
ANDROID_VR_NO_AUTH.clientVersion,
27-
true),
19+
false,
20+
"Android VR No auth"
21+
),
2822
ANDROID_UNPLUGGED(
2923
29,
3024
"ANDROID_UNPLUGGED",
@@ -33,7 +27,49 @@ public enum ClientType {
3327
"com.google.android.apps.youtube.unplugged/8.49.0 (Linux; U; Android 14; GB) gzip",
3428
"34",
3529
"8.49.0",
36-
true); // Requires login.
30+
true,
31+
"Android TV"
32+
),
33+
ANDROID_VR(
34+
ANDROID_VR_NO_AUTH.id,
35+
ANDROID_VR_NO_AUTH.clientName,
36+
ANDROID_VR_NO_AUTH.deviceModel,
37+
ANDROID_VR_NO_AUTH.osVersion,
38+
ANDROID_VR_NO_AUTH.userAgent,
39+
ANDROID_VR_NO_AUTH.androidSdkVersion,
40+
ANDROID_VR_NO_AUTH.clientVersion,
41+
true,
42+
"Android VR"
43+
),
44+
IOS_UNPLUGGED(33,
45+
"IOS_UNPLUGGED",
46+
forceAVC()
47+
? "iPhone12,5" // 11 Pro Max (last device with iOS 13)
48+
: "iPhone16,2", // 15 Pro Max
49+
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
50+
forceAVC()
51+
? "13.7.17H35" // Last release of iOS 13.
52+
: "18.1.1.22B91",
53+
forceAVC()
54+
? "com.google.ios.youtubeunplugged/6.45 (iPhone; U; CPU iOS 13_7 like Mac OS X)"
55+
: "com.google.ios.youtubeunplugged/8.33 (iPhone; U; CPU iOS 18_1_1 like Mac OS X)",
56+
null,
57+
// Version number should be a valid iOS release.
58+
// https://www.ipa4fun.com/history/152043/
59+
// Some newer versions can also force AVC,
60+
// but 6.45 is the last version that supports iOS 13.
61+
forceAVC()
62+
? "6.45"
63+
: "8.33",
64+
true,
65+
forceAVC()
66+
? "iOS TV Force AVC"
67+
: "iOS TV"
68+
);
69+
70+
private static boolean forceAVC() {
71+
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
72+
}
3773

3874
/**
3975
* YouTube
@@ -75,14 +111,20 @@ public enum ClientType {
75111
*/
76112
public final boolean canLogin;
77113

114+
/**
115+
* Friendly name displayed in stats for nerds.
116+
*/
117+
public final String friendlyName;
118+
78119
ClientType(int id,
79120
String clientName,
80121
String deviceModel,
81122
String osVersion,
82123
String userAgent,
83124
@Nullable String androidSdkVersion,
84125
String clientVersion,
85-
boolean canLogin) {
126+
boolean canLogin,
127+
String friendlyName) {
86128
this.id = id;
87129
this.clientName = clientName;
88130
this.deviceModel = deviceModel;
@@ -91,5 +133,7 @@ public enum ClientType {
91133
this.androidSdkVersion = androidSdkVersion;
92134
this.clientVersion = clientVersion;
93135
this.canLogin = canLogin;
136+
this.friendlyName = friendlyName;
94137
}
138+
95139
}

extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java

+61-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package app.revanced.extension.shared.spoof;
22

33
import android.net.Uri;
4+
import android.text.TextUtils;
45

56
import androidx.annotation.Nullable;
67

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

21+
private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA
22+
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
23+
2024
/**
2125
* Any unreachable ip address. Used to intentionally fail requests.
2226
*/
@@ -30,17 +34,6 @@ private static boolean isPatchIncluded() {
3034
return false; // Modified during patching.
3135
}
3236

33-
public static final class NotSpoofingAndroidAvailability implements Setting.Availability {
34-
@Override
35-
public boolean isAvailable() {
36-
if (SpoofVideoStreamsPatch.isPatchIncluded()) {
37-
return !BaseSettings.SPOOF_VIDEO_STREAMS.get();
38-
}
39-
40-
return true;
41-
}
42-
}
43-
4437
/**
4538
* Injection point.
4639
* Blocks /get_watch requests by returning an unreachable URI.
@@ -97,6 +90,17 @@ public static boolean isSpoofingEnabled() {
9790
return SPOOF_STREAMING_DATA;
9891
}
9992

93+
/**
94+
* Injection point.
95+
* Only invoked when playing a livestream on an iOS client.
96+
*/
97+
public static boolean fixHLSCurrentTime(boolean original) {
98+
if (!SPOOF_STREAMING_DATA) {
99+
return original;
100+
}
101+
return false;
102+
}
103+
100104
/**
101105
* Injection point.
102106
*/
@@ -183,4 +187,50 @@ public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] pos
183187

184188
return postData;
185189
}
190+
191+
/**
192+
* Injection point.
193+
*/
194+
public static String appendSpoofedClient(String videoFormat) {
195+
try {
196+
if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
197+
&& !TextUtils.isEmpty(videoFormat)) {
198+
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages.
199+
return "\u202D" + videoFormat + "\u2009(" // u202D = left to right override
200+
+ StreamingDataRequest.getLastSpoofedClientName() + ")";
201+
}
202+
} catch (Exception ex) {
203+
Logger.printException(() -> "appendSpoofedClient failure", ex);
204+
}
205+
206+
return videoFormat;
207+
}
208+
209+
public static final class NotSpoofingAndroidAvailability implements Setting.Availability {
210+
@Override
211+
public boolean isAvailable() {
212+
if (SpoofVideoStreamsPatch.isPatchIncluded()) {
213+
return !BaseSettings.SPOOF_VIDEO_STREAMS.get()
214+
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
215+
}
216+
217+
return true;
218+
}
219+
}
220+
221+
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
222+
@Override
223+
public boolean isAvailable() {
224+
return !BaseSettings.SPOOF_VIDEO_STREAMS.get()
225+
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_VR_NO_AUTH;
226+
}
227+
}
228+
229+
public static final class SpoofiOSAvailability implements Setting.Availability {
230+
@Override
231+
public boolean isAvailable() {
232+
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
233+
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
234+
}
235+
}
186236
}

extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import app.revanced.extension.shared.requests.Requester;
1111
import app.revanced.extension.shared.requests.Route;
1212
import app.revanced.extension.shared.settings.BaseSettings;
13+
import app.revanced.extension.shared.spoof.AudioStreamLanguage;
1314
import app.revanced.extension.shared.spoof.ClientType;
1415

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

40+
// Can override default language only if no login is used.
41+
// Could use preferred audio for all clients that do not login,
42+
// but if this is a fall over client it will set the language even though
43+
// the audio language is not selectable in the UI.
44+
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
45+
AudioStreamLanguage language = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH
46+
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get()
47+
: AudioStreamLanguage.DEFAULT;
48+
3949
JSONObject client = new JSONObject();
40-
client.put("hl", BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLanguage());
50+
client.put("hl", language.getLanguage());
4151
client.put("clientName", clientType.clientName);
4252
client.put("clientVersion", clientType.clientVersion);
4353
client.put("deviceModel", clientType.deviceModel);

extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java

+27-10
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import app.revanced.extension.shared.Logger;
2323
import app.revanced.extension.shared.Utils;
2424
import app.revanced.extension.shared.settings.BaseSettings;
25-
import app.revanced.extension.shared.spoof.AudioStreamLanguage;
2625
import app.revanced.extension.shared.spoof.ClientType;
2726

2827
/**
@@ -36,7 +35,22 @@
3635
*/
3736
public class StreamingDataRequest {
3837

39-
private static final ClientType[] CLIENT_ORDER_TO_USE = ClientType.values();
38+
private static final ClientType[] CLIENT_ORDER_TO_USE;
39+
40+
static {
41+
ClientType[] allClientTypes = ClientType.values();
42+
ClientType preferredClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
43+
44+
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
45+
CLIENT_ORDER_TO_USE[0] = preferredClient;
46+
47+
int i = 1;
48+
for (ClientType c : allClientTypes) {
49+
if (c != preferredClient) {
50+
CLIENT_ORDER_TO_USE[i++] = c;
51+
}
52+
}
53+
}
4054

4155
private static final String AUTHORIZATION_HEADER = "Authorization";
4256

@@ -73,6 +87,13 @@ protected boolean removeEldestEntry(Entry eldest) {
7387
}
7488
});
7589

90+
private static volatile ClientType lastSpoofedClientType;
91+
92+
public static String getLastSpoofedClientName() {
93+
ClientType client = lastSpoofedClientType;
94+
return client == null ? "Unknown" : client.friendlyName;
95+
}
96+
7697
private final String videoId;
7798

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

167-
if (clientType == ClientType.ANDROID_VR_NO_AUTH
168-
&& BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get() == AudioStreamLanguage.DEFAULT) {
169-
// Only use no auth Android VR if a non default audio language is selected.
170-
continue;
171-
}
172-
173188
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
174189
if (connection != null) {
175190
try {
176191
// gzip encoding doesn't response with content length (-1),
177192
// but empty response body does.
178193
if (connection.getContentLength() == 0) {
179194
if (BaseSettings.DEBUG.get()) {
180-
Logger.printException(() -> "Ignoring empty client response: " + clientType);
195+
Logger.printException(() -> "Ignoring empty client: " + clientType);
181196
}
182197
} else {
183198
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
@@ -188,6 +203,7 @@ private static ByteBuffer fetch(String videoId, Map<String, String> playerHeader
188203
while ((bytesRead = inputStream.read(buffer)) >= 0) {
189204
baos.write(buffer, 0, bytesRead);
190205
}
206+
lastSpoofedClientType = clientType;
191207

192208
return ByteBuffer.wrap(baos.toByteArray());
193209
}
@@ -198,7 +214,8 @@ private static ByteBuffer fetch(String videoId, Map<String, String> playerHeader
198214
}
199215
}
200216

201-
handleConnectionError("Could not fetch any client streams", null, debugEnabled);
217+
lastSpoofedClientType = null;
218+
handleConnectionError("Could not fetch any client streams", null, true);
202219
return null;
203220
}
204221

0 commit comments

Comments
 (0)