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:
LisoUseInAIKyrios
2025-09-15 20:58:56 +04:00
committed by GitHub
parent cb6d802de3
commit abe3943f98
28 changed files with 223 additions and 253 deletions

View File

@@ -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_43_32;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48; 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.ClientType;
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest; import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
@@ -13,10 +16,11 @@ public class SpoofVideoStreamsPatch {
* Injection point. * Injection point.
*/ */
public static void setClientOrderToUse() { public static void setClientOrderToUse() {
ClientType[] availableClients = { List<ClientType> availableClients = List.of(
ANDROID_VR_1_43_32, ANDROID_VR_1_43_32,
ANDROID_VR_1_61_48, ANDROID_VR_1_61_48,
}; VISIONOS
);
StreamingDataRequest.setClientOrderToUse(availableClients, ANDROID_VR_1_43_32); StreamingDataRequest.setClientOrderToUse(availableClients, ANDROID_VR_1_43_32);
} }

View File

@@ -4,7 +4,6 @@ import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent; 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.AudioStreamLanguageOverrideAvailability;
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability;
import app.revanced.extension.shared.spoof.ClientType; 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 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 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_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. // 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)); 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));
} }

View File

@@ -2,15 +2,19 @@ package app.revanced.extension.shared.spoof;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BaseSettings;
public enum ClientType { 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 // https://dumps.tadiphone.dev/dumps/oculus/eureka
ANDROID_VR_1_61_48( ANDROID_VR_1_61_48(
28, 28,
@@ -29,27 +33,30 @@ public enum ClientType {
false, false,
"Android VR 1.61" "Android VR 1.61"
), ),
// Chromecast with Google TV 4K. /**
// https://dumps.tadiphone.dev/dumps/google/kirkwood * Uses non adaptive bitrate, which fixes audio stuttering with YT Music.
ANDROID_UNPLUGGED( * Does not use AV1.
29, */
"ANDROID_UNPLUGGED", ANDROID_VR_1_43_32(
"com.google.android.apps.youtube.unplugged", ANDROID_VR_1_61_48.id,
"Google", ANDROID_VR_1_61_48.clientName,
"Google TV Streamer", Objects.requireNonNull(ANDROID_VR_1_61_48.packageName),
"Android", ANDROID_VR_1_61_48.deviceMake,
"14", ANDROID_VR_1_61_48.deviceModel,
"34", ANDROID_VR_1_61_48.osName,
"UTT3.240625.001.K5", ANDROID_VR_1_61_48.osVersion,
"132.0.6808.3", Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
"8.49.0", Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
true, "107.0.5284.2",
true, "1.43.32",
"Android TV" 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 * Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
// https://dumps.tadiphone.dev/dumps/google/barbet * <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
*/
ANDROID_CREATOR( ANDROID_CREATOR(
14, 14,
"ANDROID_CREATOR", "ANDROID_CREATOR",
@@ -66,62 +73,22 @@ public enum ClientType {
true, true,
"Android Creator" "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. * Internal YT client for an unreleased YT client. May stop working at any time.
* Uses VP9 and not AV1.
*/ */
ANDROID_VR_1_43_32( VISIONOS(101,
ANDROID_VR_1_61_48.id, "VISIONOS",
ANDROID_VR_1_61_48.clientName, "Apple",
ANDROID_VR_1_61_48.packageName, "RealityDevice14,1",
ANDROID_VR_1_61_48.deviceMake, "visionOS",
ANDROID_VR_1_61_48.deviceModel, "1.3.21O771",
ANDROID_VR_1_61_48.osName, "0.1",
ANDROID_VR_1_61_48.osVersion, "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",
ANDROID_VR_1_61_48.androidSdkVersion, false,
ANDROID_VR_1_61_48.buildId, false,
"107.0.5284.2", "visionOS"
"1.43.32",
ANDROID_VR_1_61_48.requiresAuth,
ANDROID_VR_1_61_48.useAuth,
"Android VR 1.43"
); );
private static boolean forceAVC() {
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
}
/** /**
* YouTube * YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a> * <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. * App package name.
*/ */
@Nullable
private final String packageName; private final String packageName;
/** /**
@@ -202,17 +170,20 @@ public enum ClientType {
*/ */
public final String friendlyName; public final String friendlyName;
/**
* Android constructor.
*/
@SuppressWarnings("ConstantLocale") @SuppressWarnings("ConstantLocale")
ClientType(int id, ClientType(int id,
String clientName, String clientName,
String packageName, @NonNull String packageName,
String deviceMake, String deviceMake,
String deviceModel, String deviceModel,
String osName, String osName,
String osVersion, String osVersion,
@Nullable String androidSdkVersion, @NonNull String androidSdkVersion,
@Nullable String buildId, @NonNull String buildId,
@Nullable String cronetVersion, @NonNull String cronetVersion,
String clientVersion, String clientVersion,
boolean requiresAuth, boolean requiresAuth,
boolean useAuth, boolean useAuth,
@@ -233,31 +204,44 @@ public enum ClientType {
this.friendlyName = friendlyName; this.friendlyName = friendlyName;
Locale defaultLocale = Locale.getDefault(); Locale defaultLocale = Locale.getDefault();
if (androidSdkVersion == null) { this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
// Convert version from '18.2.22C152' into '18_2_22' packageName,
String userAgentOsVersion = osVersion clientVersion,
.replaceAll("(\\d+\\.\\d+\\.\\d+).*", "$1") osVersion,
.replace(".", "_"); defaultLocale,
// https://github.com/mitmproxy/mitmproxy/issues/4836 deviceModel,
this.userAgent = String.format("%s/%s (%s; U; CPU iOS %s like Mac OS X; %s)", Objects.requireNonNull(buildId),
packageName, Objects.requireNonNull(cronetVersion)
clientVersion, );
deviceModel,
userAgentOsVersion,
defaultLocale
);
} else {
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
packageName,
clientVersion,
osVersion,
defaultLocale,
deviceModel,
Objects.requireNonNull(buildId),
Objects.requireNonNull(cronetVersion)
);
}
Logger.printDebug(() -> "userAgent: " + this.userAgent); 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;
}
} }

View File

@@ -10,6 +10,7 @@ import java.util.Map;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; 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.BaseSettings;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest; 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 SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA 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. * Domain used for internet connectivity verification.
@@ -43,10 +47,21 @@ public class SpoofVideoStreamsPatch {
return false; // Modified during patching. return false; // Modified during patching.
} }
public static boolean notSpoofingToAndroid() { public static boolean spoofingToClientWithNoMultiAudioStreams() {
return !isPatchIncluded() return isPatchIncluded() && BaseSettings.SPOOF_VIDEO_STREAMS.get();
|| !BaseSettings.SPOOF_VIDEO_STREAMS.get() }
|| BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED;
/**
* @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 { public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
@Override @Override
public boolean isAvailable() { public boolean isAvailable() {
ClientType clientType = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); // Since all current clients are un-authenticated, this works for all spoof clients.
return BaseSettings.SPOOF_VIDEO_STREAMS.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;
} }
} }
} }

View File

@@ -1,5 +1,7 @@
package app.revanced.extension.shared.spoof.requests; 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.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@@ -10,8 +12,10 @@ import java.util.Locale;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route; 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.settings.BaseSettings;
import app.revanced.extension.shared.spoof.ClientType; import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
final class PlayerRoutes { final class PlayerRoutes {
static final Route.CompiledRoute GET_STREAMING_DATA = new Route( static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
@@ -37,15 +41,16 @@ final class PlayerRoutes {
try { try {
JSONObject context = new JSONObject(); JSONObject context = new JSONObject();
// Can override default language only if no login is used. AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
// Could use preferred audio for all clients that do not login, if (language == null || BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ANDROID_VR_1_43_32) {
// but if this is a fall over client it will set the language even though // Force original audio has not overrode the language.
// the audio language is not selectable in the UI. // Or if YT has fallen over to the very last client (VR 1.43), then always
ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); // use the app language because forcing an audio stream of specific languages
Locale streamLocale = (userSelectedClient == ClientType.ANDROID_VR_1_61_48 // can sometimes fail so it's better to try and load something rather than nothing.
|| userSelectedClient == ClientType.ANDROID_VR_1_43_32) language = BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get();
? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get().getLocale() }
: Locale.getDefault(); //noinspection ExtractMethodRecommender
Locale streamLocale = language.getLocale();
JSONObject client = new JSONObject(); JSONObject client = new JSONObject();
client.put("deviceMake", clientType.deviceMake); client.put("deviceMake", clientType.deviceMake);

View File

@@ -37,10 +37,15 @@ public class StreamingDataRequest {
private static volatile ClientType[] clientOrderToUse = ClientType.values(); private static volatile ClientType[] clientOrderToUse = ClientType.values();
public static void setClientOrderToUse(ClientType[] availableClients, ClientType preferredClient) { public static void setClientOrderToUse(List<ClientType> availableClients, ClientType preferredClient) {
Objects.requireNonNull(availableClients); Objects.requireNonNull(preferredClient);
clientOrderToUse = new ClientType[availableClients.length]; int availableClientSize = availableClients.size();
if (!availableClients.contains(preferredClient)) {
availableClientSize++;
}
clientOrderToUse = new ClientType[availableClientSize];
clientOrderToUse[0] = preferredClient; clientOrderToUse[0] = preferredClient;
int i = 1; int i = 1;

View File

@@ -1,7 +1,7 @@
package app.revanced.extension.youtube.patches; package app.revanced.extension.youtube.patches;
import app.revanced.extension.shared.Logger; 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.shared.spoof.SpoofVideoStreamsPatch;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@@ -11,16 +11,20 @@ public class ForceOriginalAudioPatch {
private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4"; 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 void setPreferredLanguage() {
if (Settings.FORCE_ORIGINAL_AUDIO.get()) {
public static final class ForceOriginalAudioAvailability implements Setting.Availability { // None of the current spoof clients support audio track menu,
@Override // And all are un-authenticated and can request any language code
public boolean isAvailable() { // (authenticated requests ignore the language code and always use the account language).
// Check conditions of launch and now. Otherwise if spoofing is changed // To still support force original audio, if it's enabled then pick a language
// without a restart the setting will show as available when it's not. // that is not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972
return PATCH_AVAILABLE && SpoofVideoStreamsPatch.notSpoofingToAndroid(); // 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);
} }
} }

View File

@@ -9,13 +9,13 @@ import app.revanced.extension.youtube.shared.ShortsPlayerState;
public class PlayerFlyoutMenuItemsFilter extends Filter { public class PlayerFlyoutMenuItemsFilter extends Filter {
public static final class HideAudioFlyoutMenuAvailability implements Setting.Availability { 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 @Override
public boolean isAvailable() { public boolean isAvailable() {
// Check conditions of launch and now. Otherwise if spoofing is changed // Check conditions of launch and now. Otherwise if spoofing is changed
// without a restart the setting will show as available when it's not. // 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();
} }
} }

View File

@@ -1,9 +1,11 @@
package app.revanced.extension.youtube.patches.spoof; 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_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.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.settings.BaseSettings;
import app.revanced.extension.shared.spoof.ClientType; import app.revanced.extension.shared.spoof.ClientType;
@@ -16,12 +18,13 @@ public class SpoofVideoStreamsPatch {
* Injection point. * Injection point.
*/ */
public static void setClientOrderToUse() { public static void setClientOrderToUse() {
ClientType[] availableClients = { List<ClientType> availableClients = List.of(
ANDROID_VR_1_61_48, ANDROID_VR_1_61_48,
ANDROID_UNPLUGGED,
ANDROID_CREATOR, 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, StreamingDataRequest.setClientOrderToUse(availableClients,
BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()); BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());

View File

@@ -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.ChangeStartPageTypeAvailability;
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage; 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.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.MiniplayerHorizontalDragAvailability;
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType;
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MINIMAL; 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); "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 // 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 // Ads
public static final BooleanSetting HIDE_CREATOR_STORE_SHELF = new BooleanSetting("revanced_hide_creator_store_shelf", TRUE); public static final BooleanSetting HIDE_CREATOR_STORE_SHELF = new BooleanSetting("revanced_hide_creator_store_shelf", TRUE);

View File

@@ -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);
}
}

View File

@@ -12,8 +12,8 @@ import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
public class HideAudioFlyoutMenuPreference extends SwitchPreference { public class HideAudioFlyoutMenuPreference extends SwitchPreference {
{ {
// Audio menu is not available if spoofing to Android client type. // Audio menu is not available if spoofing to most client types.
if (!SpoofVideoStreamsPatch.notSpoofingToAndroid()) { if (SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams()) {
String summary = str("revanced_hide_player_flyout_audio_track_not_available"); String summary = str("revanced_hide_player_flyout_audio_track_not_available");
setSummary(summary); setSummary(summary);
setSummaryOn(summary); setSummaryOn(summary);

View File

@@ -78,21 +78,17 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
Logger.printDebug(() -> "Updating spoof stream side effects preference"); Logger.printDebug(() -> "Updating spoof stream side effects preference");
setEnabled(BaseSettings.SPOOF_VIDEO_STREAMS.get()); setEnabled(BaseSettings.SPOOF_VIDEO_STREAMS.get());
String key = "revanced_spoof_video_streams_about_" + String title = str("revanced_spoof_video_streams_about_title");
(clientType == ClientType.IOS_UNPLUGGED // Currently only Android VR and VisionOS are supported, and both have the same base side effects.
? "ios_tv" String summary = str("revanced_spoof_video_streams_about_android_summary");
: "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");
}
summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos"); 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); setTitle(title);
setSummary(summary); setSummary(summary);
} }

View File

@@ -89,7 +89,7 @@ val downloadsPatch = bytecodePatch(
// Main activity is used to launch downloader intent. // Main activity is used to launch downloader intent.
mainActivityOnCreateFingerprint.method.addInstruction( mainActivityOnCreateFingerprint.method.addInstruction(
1, 0,
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V" "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V"
) )

View File

@@ -1,6 +1,7 @@
package app.revanced.patches.youtube.layout.seekbar package app.revanced.patches.youtube.layout.seekbar
import app.revanced.patcher.fingerprint 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.containsLiteralInstruction
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstruction
@@ -103,7 +104,7 @@ internal val launchScreenLayoutTypeFingerprint = fingerprint {
custom { method, _ -> custom { method, _ ->
val firstParameter = method.parameterTypes.firstOrNull() val firstParameter = method.parameterTypes.firstOrNull()
// 19.25 - 19.45 // 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+ || firstParameter == "Landroid/app/Activity;") // 19.46+
&& method.containsLiteralInstruction(launchScreenLayoutTypeLotteFeatureFlag) && method.containsLiteralInstruction(launchScreenLayoutTypeLotteFeatureFlag)
} }

View File

@@ -68,7 +68,7 @@ val shortsAutoplayPatch = bytecodePatch(
// Main activity is used to check if app is in pip mode. // Main activity is used to check if app is in pip mode.
mainActivityOnCreateFingerprint.method.addInstruction( mainActivityOnCreateFingerprint.method.addInstruction(
1, 0,
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->setMainActivity(Landroid/app/Activity;)V", "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->setMainActivity(Landroid/app/Activity;)V",
) )

View File

@@ -90,8 +90,8 @@ val openShortsInRegularPlayerPatch = bytecodePatch(
// Activity is used as the context to launch an Intent. // Activity is used as the context to launch an Intent.
mainActivityOnCreateFingerprint.method.addInstruction( mainActivityOnCreateFingerprint.method.addInstruction(
1, 0,
"invoke-static/range { p0 .. p0 }, ${EXTENSION_CLASS_DESCRIPTOR}->" + "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" +
"setMainActivity(Landroid/app/Activity;)V", "setMainActivity(Landroid/app/Activity;)V",
) )

View File

@@ -1,6 +1,7 @@
package app.revanced.patches.youtube.layout.theme package app.revanced.patches.youtube.layout.theme
import app.revanced.patcher.fingerprint import app.revanced.patcher.fingerprint
import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
import app.revanced.util.literal import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@@ -37,6 +38,6 @@ internal val splashScreenStyleFingerprint = fingerprint {
parameters("Landroid/os/Bundle;") parameters("Landroid/os/Bundle;")
literal { SPLASH_SCREEN_STYLE_FEATURE_FLAG } literal { SPLASH_SCREEN_STYLE_FEATURE_FLAG }
custom { method, classDef -> custom { method, classDef ->
method.name == "onCreate" && classDef.endsWith("/MainActivity;") method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
} }
} }

View File

@@ -40,9 +40,7 @@ val announcementsPatch = bytecodePatch(
) )
mainActivityOnCreateFingerprint.method.addInstruction( mainActivityOnCreateFingerprint.method.addInstruction(
// Insert index must be greater than the insert index used by GmsCoreSupport, 0,
// as both patch the same method and GmsCore check should be first.
1,
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V", "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V",
) )
} }

View File

@@ -34,12 +34,7 @@ val checkWatchHistoryDomainNameResolutionPatch = bytecodePatch(
addResources("youtube", "misc.dns.checkWatchHistoryDomainNameResolutionPatch") addResources("youtube", "misc.dns.checkWatchHistoryDomainNameResolutionPatch")
mainActivityOnCreateFingerprint.method.addInstruction( mainActivityOnCreateFingerprint.method.addInstruction(
// FIXME: Insert index must be greater than the insert index used by GmsCoreSupport, 0,
// 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,
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->checkDnsResolver(Landroid/app/Activity;)V", "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->checkDnsResolver(Landroid/app/Activity;)V",
) )
} }

View File

@@ -3,4 +3,5 @@ package app.revanced.patches.youtube.misc.extension
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.extension.hooks.* import app.revanced.patches.youtube.misc.extension.hooks.*
val sharedExtensionPatch = sharedExtensionPatch("youtube", applicationInitHook) val sharedExtensionPatch = sharedExtensionPatch("youtube",
applicationInitHook, applicationInitOnCrateHook)

View File

@@ -1,11 +1,23 @@
package app.revanced.patches.youtube.misc.extension.hooks package app.revanced.patches.youtube.misc.extension.hooks
import app.revanced.patches.shared.misc.extension.extensionHook 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). * 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. // Extension context is the Activity itself.
internal val applicationInitHook = extensionHook { 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") 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
}
}

View File

@@ -127,8 +127,7 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig
// Litho filtering based on navigation tab before the tab is updated. // Litho filtering based on navigation tab before the tab is updated.
mainActivityOnBackPressedFingerprint.method.addInstruction( mainActivityOnBackPressedFingerprint.method.addInstruction(
0, 0,
"invoke-static { p0 }, " + "invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->onBackPressed(Landroid/app/Activity;)V",
"$EXTENSION_CLASS_DESCRIPTOR->onBackPressed(Landroid/app/Activity;)V",
) )
// Hook the search bar. // Hook the search bar.

View File

@@ -69,14 +69,13 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
entryValuesKey = "revanced_language_entry_values", entryValuesKey = "revanced_language_entry_values",
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference" tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
), ),
SwitchPreference("revanced_spoof_video_streams_ios_force_avc"),
SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"), SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"),
), )
), )
) )
mainActivityOnCreateFingerprint.method.addInstruction( mainActivityOnCreateFingerprint.method.addInstruction(
1, // Must use 1 index so context is set by extension patch., 0,
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setClientOrderToUse()V" "invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setClientOrderToUse()V"
) )
} }

View File

@@ -4,6 +4,8 @@ import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode 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 { internal val conversionContextFingerprintToString = fingerprint {
parameters() parameters()
strings( strings(
@@ -48,7 +50,7 @@ internal val mainActivityConstructorFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameters() parameters()
custom { _, classDef -> custom { _, classDef ->
classDef.endsWith("/MainActivity;") classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
} }
} }
@@ -57,7 +59,7 @@ internal val mainActivityOnBackPressedFingerprint = fingerprint {
returns("V") returns("V")
parameters() parameters()
custom { method, classDef -> 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") returns("V")
parameters("Landroid/os/Bundle;") parameters("Landroid/os/Bundle;")
custom { method, classDef -> custom { method, classDef ->
method.name == "onCreate" && classDef.endsWith("/MainActivity;") method.name == "onCreate" && classDef.type == YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE
} }
} }

View File

@@ -1,5 +1,6 @@
package app.revanced.patches.youtube.video.audio 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.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction 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.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint
import app.revanced.util.findMethodFromToString import app.revanced.util.findMethodFromToString
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.insertLiteralOverride import app.revanced.util.insertLiteralOverride
@@ -55,10 +57,12 @@ val forceOriginalAudioPatch = bytecodePatch(
addResources("youtube", "video.audio.forceOriginalAudioPatch") addResources("youtube", "video.audio.forceOriginalAudioPatch")
PreferenceScreen.VIDEO.addPreferences( PreferenceScreen.VIDEO.addPreferences(
SwitchPreference( SwitchPreference("revanced_force_original_audio")
key = "revanced_force_original_audio", )
tag = "app.revanced.extension.youtube.settings.preference.ForceOriginalAudioSwitchPreference"
) mainActivityOnCreateFingerprint.method.addInstruction(
0,
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setPreferredLanguage()V"
) )
// Disable feature flag that ignores the default track flag // Disable feature flag that ignores the default track flag

View File

@@ -125,14 +125,12 @@
<app id="youtube"> <app id="youtube">
<patch id="misc.fix.playback.spoofVideoStreamsPatch"> <patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string-array name="revanced_spoof_video_streams_client_type_entries"> <string-array name="revanced_spoof_video_streams_client_type_entries">
<item>Android TV</item>
<item>Android VR</item> <item>Android VR</item>
<item>iOS TV</item> <item>VisionOS</item>
</string-array> </string-array>
<string-array name="revanced_spoof_video_streams_client_type_entry_values"> <string-array name="revanced_spoof_video_streams_client_type_entry_values">
<item>ANDROID_UNPLUGGED</item>
<item>ANDROID_VR_1_61_48</item> <item>ANDROID_VR_1_61_48</item>
<item>IOS_UNPLUGGED</item> <item>VISIONOS</item>
</string-array> </string-array>
</patch> </patch>
<patch id="interaction.swipecontrols.swipeControlsResourcePatch"> <patch id="interaction.swipecontrols.swipeControlsResourcePatch">

View File

@@ -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'. --> <!-- '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 <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. --> <!-- '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_title">Hide Watch in VR</string>
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Watch in VR menu is hidden</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_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_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_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 <string name="revanced_spoof_video_streams_summary_off">"Video streams are not spoofed
Video playback may not work"</string> 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_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_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_about_title">Spoofing side effects</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_android_title">Android 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 <string name="revanced_spoof_video_streams_about_android_summary">"• Audio track menu is missing
• Stable volume is not available • Stable volume is not available"</string>
• Force original audio 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_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_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_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_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_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> </patch>
</app> </app>
<app id="twitch"> <app id="twitch">