mirror of
https://github.com/ReVanced/revanced-patches.git
synced 2026-01-11 13:46:17 +00:00
fix(Spoof video streams): Remove Android TV and iOS TV clients, add experimental VisionOS, add temporary fix for Force original audio to work with any spoof client (#5861)
This commit is contained in:
committed by
GitHub
parent
cb6d802de3
commit
abe3943f98
@@ -2,6 +2,9 @@ package app.revanced.extension.music.patches.spoof;
|
||||
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||
@@ -13,10 +16,11 @@ public class SpoofVideoStreamsPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setClientOrderToUse() {
|
||||
ClientType[] availableClients = {
|
||||
List<ClientType> availableClients = List.of(
|
||||
ANDROID_VR_1_43_32,
|
||||
ANDROID_VR_1_61_48,
|
||||
};
|
||||
VISIONOS
|
||||
);
|
||||
|
||||
StreamingDataRequest.setClientOrderToUse(availableClients, ANDROID_VR_1_43_32);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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.ClientType;
|
||||
|
||||
@@ -31,8 +30,6 @@ public class BaseSettings {
|
||||
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<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||
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_1_61_48, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
}
|
||||
|
||||
@@ -2,15 +2,19 @@ package app.revanced.extension.shared.spoof;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
public enum ClientType {
|
||||
/**
|
||||
* Video not playable: Kids / Paid / Movie / Private / Age-restricted.
|
||||
* This client can only be used when logged out.
|
||||
*/
|
||||
// https://dumps.tadiphone.dev/dumps/oculus/eureka
|
||||
ANDROID_VR_1_61_48(
|
||||
28,
|
||||
@@ -29,27 +33,30 @@ public enum ClientType {
|
||||
false,
|
||||
"Android VR 1.61"
|
||||
),
|
||||
// Chromecast with Google TV 4K.
|
||||
// https://dumps.tadiphone.dev/dumps/google/kirkwood
|
||||
ANDROID_UNPLUGGED(
|
||||
29,
|
||||
"ANDROID_UNPLUGGED",
|
||||
"com.google.android.apps.youtube.unplugged",
|
||||
"Google",
|
||||
"Google TV Streamer",
|
||||
"Android",
|
||||
"14",
|
||||
"34",
|
||||
"UTT3.240625.001.K5",
|
||||
"132.0.6808.3",
|
||||
"8.49.0",
|
||||
true,
|
||||
true,
|
||||
"Android TV"
|
||||
/**
|
||||
* Uses non adaptive bitrate, which fixes audio stuttering with YT Music.
|
||||
* Does not use AV1.
|
||||
*/
|
||||
ANDROID_VR_1_43_32(
|
||||
ANDROID_VR_1_61_48.id,
|
||||
ANDROID_VR_1_61_48.clientName,
|
||||
Objects.requireNonNull(ANDROID_VR_1_61_48.packageName),
|
||||
ANDROID_VR_1_61_48.deviceMake,
|
||||
ANDROID_VR_1_61_48.deviceModel,
|
||||
ANDROID_VR_1_61_48.osName,
|
||||
ANDROID_VR_1_61_48.osVersion,
|
||||
Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
|
||||
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
|
||||
"107.0.5284.2",
|
||||
"1.43.32",
|
||||
ANDROID_VR_1_61_48.requiresAuth,
|
||||
ANDROID_VR_1_61_48.useAuth,
|
||||
"Android VR 1.43"
|
||||
),
|
||||
// Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||
// Google Pixel 9 Pro Fold
|
||||
// https://dumps.tadiphone.dev/dumps/google/barbet
|
||||
/**
|
||||
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
|
||||
*/
|
||||
ANDROID_CREATOR(
|
||||
14,
|
||||
"ANDROID_CREATOR",
|
||||
@@ -66,62 +73,22 @@ public enum ClientType {
|
||||
true,
|
||||
"Android Creator"
|
||||
),
|
||||
IOS_UNPLUGGED(
|
||||
33,
|
||||
"IOS_UNPLUGGED",
|
||||
"com.google.ios.youtubeunplugged",
|
||||
"Apple",
|
||||
forceAVC()
|
||||
// 11 Pro Max (last device with iOS 13)
|
||||
? "iPhone12,5"
|
||||
// 15 Pro Max
|
||||
: "iPhone16,2",
|
||||
"iOS",
|
||||
forceAVC()
|
||||
// iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1.
|
||||
? "13.7.17H35"
|
||||
: "18.2.22C152",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
// Version number should be a valid iOS release.
|
||||
// https://www.ipa4fun.com/history/152043/
|
||||
forceAVC()
|
||||
// Some newer versions can also force AVC,
|
||||
// but 6.45 is the last version that supports iOS 13.
|
||||
? "6.45"
|
||||
: "8.49",
|
||||
true,
|
||||
true,
|
||||
forceAVC()
|
||||
? "iOS TV Force AVC"
|
||||
: "iOS TV"
|
||||
),
|
||||
/**
|
||||
* Uses non adaptive bitrate, which fixes audio stuttering with YT Music.
|
||||
* Uses VP9 and not AV1.
|
||||
* Internal YT client for an unreleased YT client. May stop working at any time.
|
||||
*/
|
||||
ANDROID_VR_1_43_32(
|
||||
ANDROID_VR_1_61_48.id,
|
||||
ANDROID_VR_1_61_48.clientName,
|
||||
ANDROID_VR_1_61_48.packageName,
|
||||
ANDROID_VR_1_61_48.deviceMake,
|
||||
ANDROID_VR_1_61_48.deviceModel,
|
||||
ANDROID_VR_1_61_48.osName,
|
||||
ANDROID_VR_1_61_48.osVersion,
|
||||
ANDROID_VR_1_61_48.androidSdkVersion,
|
||||
ANDROID_VR_1_61_48.buildId,
|
||||
"107.0.5284.2",
|
||||
"1.43.32",
|
||||
ANDROID_VR_1_61_48.requiresAuth,
|
||||
ANDROID_VR_1_61_48.useAuth,
|
||||
"Android VR 1.43"
|
||||
VISIONOS(101,
|
||||
"VISIONOS",
|
||||
"Apple",
|
||||
"RealityDevice14,1",
|
||||
"visionOS",
|
||||
"1.3.21O771",
|
||||
"0.1",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
||||
false,
|
||||
false,
|
||||
"visionOS"
|
||||
);
|
||||
|
||||
private static boolean forceAVC() {
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube
|
||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
||||
@@ -133,6 +100,7 @@ public enum ClientType {
|
||||
/**
|
||||
* App package name.
|
||||
*/
|
||||
@Nullable
|
||||
private final String packageName;
|
||||
|
||||
/**
|
||||
@@ -202,17 +170,20 @@ public enum ClientType {
|
||||
*/
|
||||
public final String friendlyName;
|
||||
|
||||
/**
|
||||
* Android constructor.
|
||||
*/
|
||||
@SuppressWarnings("ConstantLocale")
|
||||
ClientType(int id,
|
||||
String clientName,
|
||||
String packageName,
|
||||
@NonNull String packageName,
|
||||
String deviceMake,
|
||||
String deviceModel,
|
||||
String osName,
|
||||
String osVersion,
|
||||
@Nullable String androidSdkVersion,
|
||||
@Nullable String buildId,
|
||||
@Nullable String cronetVersion,
|
||||
@NonNull String androidSdkVersion,
|
||||
@NonNull String buildId,
|
||||
@NonNull String cronetVersion,
|
||||
String clientVersion,
|
||||
boolean requiresAuth,
|
||||
boolean useAuth,
|
||||
@@ -233,20 +204,6 @@ public enum ClientType {
|
||||
this.friendlyName = friendlyName;
|
||||
|
||||
Locale defaultLocale = Locale.getDefault();
|
||||
if (androidSdkVersion == null) {
|
||||
// Convert version from '18.2.22C152' into '18_2_22'
|
||||
String userAgentOsVersion = osVersion
|
||||
.replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1")
|
||||
.replace(".", "_");
|
||||
// https://github.com/mitmproxy/mitmproxy/issues/4836
|
||||
this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)",
|
||||
packageName,
|
||||
clientVersion,
|
||||
deviceModel,
|
||||
userAgentOsVersion,
|
||||
defaultLocale
|
||||
);
|
||||
} else {
|
||||
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
|
||||
packageName,
|
||||
clientVersion,
|
||||
@@ -256,8 +213,35 @@ public enum ClientType {
|
||||
Objects.requireNonNull(buildId),
|
||||
Objects.requireNonNull(cronetVersion)
|
||||
);
|
||||
}
|
||||
Logger.printDebug(() -> "userAgent: " + this.userAgent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantLocale")
|
||||
ClientType(int id,
|
||||
String clientName,
|
||||
String deviceMake,
|
||||
String deviceModel,
|
||||
String osName,
|
||||
String osVersion,
|
||||
String clientVersion,
|
||||
String userAgent,
|
||||
boolean requiresAuth,
|
||||
boolean useAuth,
|
||||
String friendlyName) {
|
||||
this.id = id;
|
||||
this.clientName = clientName;
|
||||
this.deviceMake = deviceMake;
|
||||
this.deviceModel = deviceModel;
|
||||
this.osName = osName;
|
||||
this.osVersion = osVersion;
|
||||
this.clientVersion = clientVersion;
|
||||
this.userAgent = userAgent;
|
||||
this.requiresAuth = requiresAuth;
|
||||
this.useAuth = useAuth;
|
||||
this.friendlyName = friendlyName;
|
||||
this.packageName = null;
|
||||
this.androidSdkVersion = null;
|
||||
this.buildId = null;
|
||||
this.cronetVersion = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||
@@ -19,7 +20,10 @@ 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;
|
||||
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.VISIONOS;
|
||||
|
||||
@Nullable
|
||||
private static volatile AppLanguage languageOverride;
|
||||
|
||||
/**
|
||||
* Domain used for internet connectivity verification.
|
||||
@@ -43,10 +47,21 @@ public class SpoofVideoStreamsPatch {
|
||||
return false; // Modified during patching.
|
||||
}
|
||||
|
||||
public static boolean notSpoofingToAndroid() {
|
||||
return !isPatchIncluded()
|
||||
|| !BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
|
||||
public static boolean spoofingToClientWithNoMultiAudioStreams() {
|
||||
return isPatchIncluded() && BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param language Language override for non-authenticated requests. If this is null then
|
||||
* {@link BaseSettings#SPOOF_VIDEO_STREAMS_LANGUAGE} is used.
|
||||
*/
|
||||
public static void setLanguageOverride(@Nullable AppLanguage language) {
|
||||
languageOverride = language;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AppLanguage getLanguageOverride() {
|
||||
return languageOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,17 +276,8 @@ public class SpoofVideoStreamsPatch {
|
||||
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
ClientType clientType = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
||||
&& (clientType == ClientType.ANDROID_VR_1_61_48 || clientType == ClientType.ANDROID_VR_1_43_32);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// Since all current clients are un-authenticated, this works for all spoof clients.
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package app.revanced.extension.shared.spoof.requests;
|
||||
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -10,8 +12,10 @@ import java.util.Locale;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||
|
||||
final class PlayerRoutes {
|
||||
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
|
||||
@@ -37,15 +41,16 @@ final class PlayerRoutes {
|
||||
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();
|
||||
Locale streamLocale = (userSelectedClient == ClientType.ANDROID_VR_1_61_48
|
||||
|| userSelectedClient == ClientType.ANDROID_VR_1_43_32)
|
||||
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale()
|
||||
: Locale.getDefault();
|
||||
AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
|
||||
if (language == null || BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ANDROID_VR_1_43_32) {
|
||||
// Force original audio has not overrode the language.
|
||||
// Or if YT has fallen over to the very last client (VR 1.43), then always
|
||||
// use the app language because forcing an audio stream of specific languages
|
||||
// can sometimes fail so it's better to try and load something rather than nothing.
|
||||
language = BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get();
|
||||
}
|
||||
//noinspection ExtractMethodRecommender
|
||||
Locale streamLocale = language.getLocale();
|
||||
|
||||
JSONObject client = new JSONObject();
|
||||
client.put("deviceMake", clientType.deviceMake);
|
||||
|
||||
@@ -37,10 +37,15 @@ public class StreamingDataRequest {
|
||||
|
||||
private static volatile ClientType[] clientOrderToUse = ClientType.values();
|
||||
|
||||
public static void setClientOrderToUse(ClientType[] availableClients, ClientType preferredClient) {
|
||||
Objects.requireNonNull(availableClients);
|
||||
public static void setClientOrderToUse(List<ClientType> availableClients, ClientType preferredClient) {
|
||||
Objects.requireNonNull(preferredClient);
|
||||
|
||||
clientOrderToUse = new ClientType[availableClients.length];
|
||||
int availableClientSize = availableClients.size();
|
||||
if (!availableClients.contains(preferredClient)) {
|
||||
availableClientSize++;
|
||||
}
|
||||
|
||||
clientOrderToUse = new ClientType[availableClientSize];
|
||||
clientOrderToUse[0] = preferredClient;
|
||||
|
||||
int i = 1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@@ -11,16 +11,20 @@ public class ForceOriginalAudioPatch {
|
||||
private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4";
|
||||
|
||||
/**
|
||||
* If the conditions to use this patch were present when the app launched.
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean PATCH_AVAILABLE = SpoofVideoStreamsPatch.notSpoofingToAndroid();
|
||||
|
||||
public static final class ForceOriginalAudioAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
// Check conditions of launch and now. Otherwise if spoofing is changed
|
||||
// without a restart the setting will show as available when it's not.
|
||||
return PATCH_AVAILABLE && SpoofVideoStreamsPatch.notSpoofingToAndroid();
|
||||
public static void setPreferredLanguage() {
|
||||
if (Settings.FORCE_ORIGINAL_AUDIO.get()) {
|
||||
// None of the current spoof clients support audio track menu,
|
||||
// And all are un-authenticated and can request any language code
|
||||
// (authenticated requests ignore the language code and always use the account language).
|
||||
// To still support force original audio, if it's enabled then pick a language
|
||||
// that is not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972
|
||||
// but the language is also supported natively by the Meta Quest device that
|
||||
// Android VR is spoofing.
|
||||
AppLanguage override = AppLanguage.SV;
|
||||
Logger.printDebug(() -> "Setting language override: " + override);
|
||||
SpoofVideoStreamsPatch.setLanguageOverride(override);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
|
||||
public static final class HideAudioFlyoutMenuAvailability implements Setting.Availability {
|
||||
private static final boolean AVAILABLE_ON_LAUNCH = SpoofVideoStreamsPatch.notSpoofingToAndroid();
|
||||
private static final boolean AVAILABLE_ON_LAUNCH = !SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams();
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
// Check conditions of launch and now. Otherwise if spoofing is changed
|
||||
// without a restart the setting will show as available when it's not.
|
||||
return AVAILABLE_ON_LAUNCH && SpoofVideoStreamsPatch.notSpoofingToAndroid();
|
||||
return AVAILABLE_ON_LAUNCH && !SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package app.revanced.extension.youtube.patches.spoof;
|
||||
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_CREATOR;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_UNPLUGGED;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.IOS_UNPLUGGED;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
@@ -16,12 +18,13 @@ public class SpoofVideoStreamsPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setClientOrderToUse() {
|
||||
ClientType[] availableClients = {
|
||||
List<ClientType> availableClients = List.of(
|
||||
ANDROID_VR_1_61_48,
|
||||
ANDROID_UNPLUGGED,
|
||||
ANDROID_CREATOR,
|
||||
IOS_UNPLUGGED
|
||||
};
|
||||
VISIONOS,
|
||||
// VR 1.43 must be last as spoof streaming data handles it slightly differently.
|
||||
ANDROID_VR_1_43_32
|
||||
);
|
||||
|
||||
StreamingDataRequest.setClientOrderToUse(availableClients,
|
||||
BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());
|
||||
|
||||
@@ -12,7 +12,6 @@ import static app.revanced.extension.youtube.patches.ChangeHeaderPatch.HeaderLog
|
||||
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability;
|
||||
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage;
|
||||
import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode;
|
||||
import static app.revanced.extension.youtube.patches.ForceOriginalAudioPatch.ForceOriginalAudioAvailability;
|
||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHorizontalDragAvailability;
|
||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType;
|
||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MINIMAL;
|
||||
@@ -75,7 +74,7 @@ public class Settings extends BaseSettings {
|
||||
"0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.5\n3.0\n4.0\n5.0\n6.0\n7.0\n8.0", true);
|
||||
|
||||
// Audio
|
||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, new ForceOriginalAudioAvailability());
|
||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, true);
|
||||
|
||||
// Ads
|
||||
public static final BooleanSetting HIDE_CREATOR_STORE_SHELF = new BooleanSetting("revanced_hide_creator_store_shelf", TRUE);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import app.revanced.extension.youtube.patches.ForceOriginalAudioPatch;
|
||||
|
||||
@SuppressWarnings({"deprecation", "unused"})
|
||||
public class ForceOriginalAudioSwitchPreference extends SwitchPreference {
|
||||
|
||||
{
|
||||
if (!ForceOriginalAudioPatch.PATCH_AVAILABLE) {
|
||||
// Show why force audio is not available.
|
||||
String summary = str("revanced_force_original_audio_not_available");
|
||||
setSummary(summary);
|
||||
setSummaryOn(summary);
|
||||
setSummaryOff(summary);
|
||||
}
|
||||
}
|
||||
|
||||
public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ForceOriginalAudioSwitchPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||
public class HideAudioFlyoutMenuPreference extends SwitchPreference {
|
||||
|
||||
{
|
||||
// Audio menu is not available if spoofing to Android client type.
|
||||
if (!SpoofVideoStreamsPatch.notSpoofingToAndroid()) {
|
||||
// Audio menu is not available if spoofing to most client types.
|
||||
if (SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams()) {
|
||||
String summary = str("revanced_hide_player_flyout_audio_track_not_available");
|
||||
setSummary(summary);
|
||||
setSummaryOn(summary);
|
||||
|
||||
@@ -78,21 +78,17 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
||||
Logger.printDebug(() -> "Updating spoof stream side effects preference");
|
||||
setEnabled(BaseSettings.SPOOF_VIDEO_STREAMS.get());
|
||||
|
||||
String key = "revanced_spoof_video_streams_about_" +
|
||||
(clientType == ClientType.IOS_UNPLUGGED
|
||||
? "ios_tv"
|
||||
: "android");
|
||||
String title = str(key + "_title");
|
||||
String summary = str(key + "_summary");
|
||||
|
||||
// Android VR supports AV1 but all other clients do not.
|
||||
if (clientType != ClientType.ANDROID_VR_1_61_48
|
||||
&& clientType != ClientType.ANDROID_VR_1_43_32) {
|
||||
summary += '\n' + str("revanced_spoof_video_streams_about_no_av1");
|
||||
}
|
||||
|
||||
String title = str("revanced_spoof_video_streams_about_title");
|
||||
// Currently only Android VR and VisionOS are supported, and both have the same base side effects.
|
||||
String summary = str("revanced_spoof_video_streams_about_android_summary");
|
||||
summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos");
|
||||
|
||||
if (clientType == ClientType.VISIONOS) {
|
||||
summary = str("revanced_spoof_video_streams_about_experimental")
|
||||
+ '\n' + summary
|
||||
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
|
||||
}
|
||||
|
||||
setTitle(title);
|
||||
setSummary(summary);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ val downloadsPatch = bytecodePatch(
|
||||
|
||||
// Main activity is used to launch downloader intent.
|
||||
mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
1,
|
||||
0,
|
||||
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.patches.youtube.layout.seekbar
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
import app.revanced.util.containsLiteralInstruction
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
@@ -103,7 +104,7 @@ internal val launchScreenLayoutTypeFingerprint = fingerprint {
|
||||
custom { method, _ ->
|
||||
val firstParameter = method.parameterTypes.firstOrNull()
|
||||
// 19.25 - 19.45
|
||||
(firstParameter == "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;"
|
||||
(firstParameter == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
|| firstParameter == "Landroid/app/Activity;") // 19.46+
|
||||
&& method.containsLiteralInstruction(launchScreenLayoutTypeLotteFeatureFlag)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ val shortsAutoplayPatch = bytecodePatch(
|
||||
|
||||
// Main activity is used to check if app is in pip mode.
|
||||
mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
1,
|
||||
0,
|
||||
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->setMainActivity(Landroid/app/Activity;)V",
|
||||
)
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ val openShortsInRegularPlayerPatch = bytecodePatch(
|
||||
|
||||
// Activity is used as the context to launch an Intent.
|
||||
mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
1,
|
||||
"invoke-static/range { p0 .. p0 }, ${EXTENSION_CLASS_DESCRIPTOR}->" +
|
||||
0,
|
||||
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"setMainActivity(Landroid/app/Activity;)V",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.patches.youtube.layout.theme
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
import app.revanced.util.literal
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
@@ -37,6 +38,6 @@ internal val splashScreenStyleFingerprint = fingerprint {
|
||||
parameters("Landroid/os/Bundle;")
|
||||
literal { SPLASH_SCREEN_STYLE_FEATURE_FLAG }
|
||||
custom { method, classDef ->
|
||||
method.name == "onCreate" && classDef.endsWith("/MainActivity;")
|
||||
method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ val announcementsPatch = bytecodePatch(
|
||||
)
|
||||
|
||||
mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
// Insert index must be greater than the insert index used by GmsCoreSupport,
|
||||
// as both patch the same method and GmsCore check should be first.
|
||||
1,
|
||||
0,
|
||||
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,12 +34,7 @@ val checkWatchHistoryDomainNameResolutionPatch = bytecodePatch(
|
||||
addResources("youtube", "misc.dns.checkWatchHistoryDomainNameResolutionPatch")
|
||||
|
||||
mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
// FIXME: Insert index must be greater than the insert index used by GmsCoreSupport,
|
||||
// as both patch the same method and GmsCoreSupport check should be first,
|
||||
// but the patch does not depend on GmsCoreSupport, so it should not be possible to enforce this
|
||||
// unless a third patch is added that this patch and GmsCoreSupport depend on to manage
|
||||
// the order of the patches.
|
||||
1,
|
||||
0,
|
||||
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->checkDnsResolver(Landroid/app/Activity;)V",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ package app.revanced.patches.youtube.misc.extension
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.misc.extension.hooks.*
|
||||
|
||||
val sharedExtensionPatch = sharedExtensionPatch("youtube", applicationInitHook)
|
||||
val sharedExtensionPatch = sharedExtensionPatch("youtube",
|
||||
applicationInitHook, applicationInitOnCrateHook)
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
package app.revanced.patches.youtube.misc.extension.hooks
|
||||
|
||||
import app.revanced.patches.shared.misc.extension.extensionHook
|
||||
import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
|
||||
/**
|
||||
* Hooks the context when the app is launched as a regular application (and is not an embedded video playback).
|
||||
*/
|
||||
// Extension context is the Activity itself.
|
||||
internal val applicationInitHook = extensionHook {
|
||||
// Does _not_ resolve to the YouTube main activity.
|
||||
// Required as some hooked code runs before the main activity is launched.
|
||||
strings("Application creation", "Application.onCreate")
|
||||
}
|
||||
|
||||
internal val applicationInitOnCrateHook = extensionHook {
|
||||
returns("V")
|
||||
parameters("Landroid/os/Bundle;")
|
||||
custom { method, classDef ->
|
||||
method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,8 +127,7 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig
|
||||
// Litho filtering based on navigation tab before the tab is updated.
|
||||
mainActivityOnBackPressedFingerprint.method.addInstruction(
|
||||
0,
|
||||
"invoke-static { p0 }, " +
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->onBackPressed(Landroid/app/Activity;)V",
|
||||
"invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->onBackPressed(Landroid/app/Activity;)V",
|
||||
)
|
||||
|
||||
// Hook the search bar.
|
||||
|
||||
@@ -69,14 +69,13 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
|
||||
entryValuesKey = "revanced_language_entry_values",
|
||||
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
|
||||
),
|
||||
SwitchPreference("revanced_spoof_video_streams_ios_force_avc"),
|
||||
SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
1, // Must use 1 index so context is set by extension patch.,
|
||||
0,
|
||||
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setClientOrderToUse()V"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal const val YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE = "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;"
|
||||
|
||||
internal val conversionContextFingerprintToString = fingerprint {
|
||||
parameters()
|
||||
strings(
|
||||
@@ -48,7 +50,7 @@ internal val mainActivityConstructorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
||||
parameters()
|
||||
custom { _, classDef ->
|
||||
classDef.endsWith("/MainActivity;")
|
||||
classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +59,7 @@ internal val mainActivityOnBackPressedFingerprint = fingerprint {
|
||||
returns("V")
|
||||
parameters()
|
||||
custom { method, classDef ->
|
||||
method.name == "onBackPressed" && classDef.endsWith("/MainActivity;")
|
||||
method.name == "onBackPressed" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +67,7 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
|
||||
returns("V")
|
||||
parameters("Landroid/os/Bundle;")
|
||||
custom { method, classDef ->
|
||||
method.name == "onCreate" && classDef.endsWith("/MainActivity;")
|
||||
method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.patches.youtube.video.audio
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
@@ -14,6 +15,7 @@ import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
|
||||
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint
|
||||
import app.revanced.util.findMethodFromToString
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.insertLiteralOverride
|
||||
@@ -55,10 +57,12 @@ val forceOriginalAudioPatch = bytecodePatch(
|
||||
addResources("youtube", "video.audio.forceOriginalAudioPatch")
|
||||
|
||||
PreferenceScreen.VIDEO.addPreferences(
|
||||
SwitchPreference(
|
||||
key = "revanced_force_original_audio",
|
||||
tag = "app.revanced.extension.youtube.settings.preference.ForceOriginalAudioSwitchPreference"
|
||||
SwitchPreference("revanced_force_original_audio")
|
||||
)
|
||||
|
||||
mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
0,
|
||||
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setPreferredLanguage()V"
|
||||
)
|
||||
|
||||
// Disable feature flag that ignores the default track flag
|
||||
|
||||
@@ -125,14 +125,12 @@
|
||||
<app id="youtube">
|
||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||
<string-array name="revanced_spoof_video_streams_client_type_entries">
|
||||
<item>Android TV</item>
|
||||
<item>Android VR</item>
|
||||
<item>iOS TV</item>
|
||||
<item>VisionOS</item>
|
||||
</string-array>
|
||||
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
|
||||
<item>ANDROID_UNPLUGGED</item>
|
||||
<item>ANDROID_VR_1_61_48</item>
|
||||
<item>IOS_UNPLUGGED</item>
|
||||
<item>VISIONOS</item>
|
||||
</string-array>
|
||||
</patch>
|
||||
<patch id="interaction.swipecontrols.swipeControlsResourcePatch">
|
||||
|
||||
@@ -765,7 +765,7 @@ If changing this setting does not take effect, try switching to Incognito mode."
|
||||
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
|
||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Audio track menu is hidden
|
||||
|
||||
To show the Audio track menu, change \'Spoof video streams\' to iOS TV"</string>
|
||||
Audio track menu is not available when \'Spoof video streams\' is enabled"</string>
|
||||
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_title">Hide Watch in VR</string>
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Watch in VR menu is hidden</string>
|
||||
@@ -1613,32 +1613,25 @@ Enabling this can unlock higher video qualities"</string>
|
||||
<string name="revanced_spoof_video_streams_screen_title">Spoof video streams</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Spoof the client video streams to prevent playback issues</string>
|
||||
<string name="revanced_spoof_video_streams_title">Spoof video streams</string>
|
||||
<string name="revanced_spoof_video_streams_summary_on">Video streams are spoofed</string>
|
||||
<string name="revanced_spoof_video_streams_summary_on">"Video streams are spoofed
|
||||
|
||||
If you are a YouTube Premium user, this setting may not be required"</string>
|
||||
<string name="revanced_spoof_video_streams_summary_off">"Video streams are not spoofed
|
||||
|
||||
Video playback may not work"</string>
|
||||
<string name="revanced_spoof_video_streams_user_dialog_message">Turning off this setting may cause video playback issues.</string>
|
||||
<string name="revanced_spoof_video_streams_client_type_title">Default client</string>
|
||||
<string name="revanced_spoof_video_streams_ios_force_avc_title">Force iOS AVC (H.264)</string>
|
||||
<string name="revanced_spoof_video_streams_ios_force_avc_summary_on">Video codec is forced to AVC (H.264)</string>
|
||||
<string name="revanced_spoof_video_streams_ios_force_avc_summary_off">Video codec is determined automatically</string>
|
||||
<string name="revanced_spoof_video_streams_ios_force_avc_user_dialog_message">"Enabling this might improve battery life and fix playback stuttering.
|
||||
|
||||
AVC has a maximum resolution of 1080p, Opus audio codec is not available, and video playback will use more internet data than VP9 or AV1."</string>
|
||||
<string name="revanced_spoof_video_streams_about_ios_tv_title">iOS spoofing side effects</string>
|
||||
<string name="revanced_spoof_video_streams_about_ios_tv_summary">"• Movies or paid videos may not play
|
||||
• Stable volume is not available
|
||||
• Videos end 1 second early"</string>
|
||||
<string name="revanced_spoof_video_streams_about_title">Spoofing side effects</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_title">Android spoofing side effects</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• Audio track menu is missing
|
||||
• Stable volume is not available
|
||||
• Force original audio is not available"</string>
|
||||
• Stable volume is not available"</string>
|
||||
<string name="revanced_spoof_video_streams_about_experimental">• Experimental client and may stop working anytime</string>
|
||||
<string name="revanced_spoof_video_streams_about_no_av1">• No AV1 video codec</string>
|
||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Kids videos may not play when logged out or in incognito mode</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_title">Show in Stats for nerds</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_on">Client type is shown in Stats for nerds</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Client is hidden in Stats for nerds</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">VR default audio stream language</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">Audio stream language</string>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="twitch">
|
||||
|
||||
Reference in New Issue
Block a user