feat(YouTube): Add Force original audio patch (#4122)

This commit is contained in:
LisoUseInAIKyrios 2024-12-15 16:51:34 +04:00 committed by GitHub
parent 1e1863446a
commit f4aa440608
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 297 additions and 69 deletions

View File

@ -3,7 +3,7 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE; 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.ForceiOSAVCAvailability; import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability;
import app.revanced.extension.shared.spoof.AudioStreamLanguage; import app.revanced.extension.shared.spoof.AudioStreamLanguage;
import app.revanced.extension.shared.spoof.ClientType; import app.revanced.extension.shared.spoof.ClientType;
@ -22,9 +22,9 @@ public class BaseSettings {
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false); public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message"); public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
public static final EnumSetting<AudioStreamLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AudioStreamLanguage.DEFAULT, parent(SPOOF_VIDEO_STREAMS)); public static final EnumSetting<AudioStreamLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AudioStreamLanguage.DEFAULT, new SpoofiOSAvailability());
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true, public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new ForceiOSAVCAvailability()); "revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability());
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS)); public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS));
} }

View File

@ -3,6 +3,11 @@ package app.revanced.extension.shared.spoof;
import java.util.Locale; import java.util.Locale;
public enum AudioStreamLanguage { public enum AudioStreamLanguage {
/**
* YouTube default.
* Can be the original language or can be app language,
* depending on what YouTube decides to pick as the default.
*/
DEFAULT, DEFAULT,
// Language codes found in locale_config.xml // Language codes found in locale_config.xml
@ -86,15 +91,21 @@ public enum AudioStreamLanguage {
private final String iso639_1; private final String iso639_1;
AudioStreamLanguage() { AudioStreamLanguage() {
iso639_1 = name().replace('_', '-'); String name = name();
final int regionSeparatorIndex = name.indexOf('_');
if (regionSeparatorIndex >= 0) {
iso639_1 = name.substring(0, regionSeparatorIndex).toLowerCase(Locale.US)
+ name.substring(regionSeparatorIndex);
} else {
iso639_1 = name().toLowerCase(Locale.US);
}
} }
public String getIso639_1() { public String getIso639_1() {
// Changing the app language does not force the app to completely restart, // Changing the app language does not force the app to completely restart,
// so the default needs to be the current language and not a static field. // so the default needs to be the current language and not a static field.
if (this == DEFAULT) { if (this == DEFAULT) {
// Android VR requires uppercase language code. return Locale.getDefault().toLanguageTag();
return Locale.getDefault().toLanguageTag().toUpperCase(Locale.US);
} }
return iso639_1; return iso639_1;

View File

@ -17,7 +17,7 @@ public enum ClientType {
"32", // Android 12.1 "32", // Android 12.1
"1.56.21", "1.56.21",
true, true,
true), false),
// Specific for kids videos. // Specific for kids videos.
IOS(5, IOS(5,
"IOS", "IOS",
@ -40,21 +40,8 @@ public enum ClientType {
? "17.40.5" ? "17.40.5"
: "19.47.7", : "19.47.7",
false, false,
true), true
/** );
* Android VR with no language code.
* Used for age restricted videos and YouTube Music to disable stable volume.
*/
ANDROID_VR_NO_HL(
ANDROID_VR.id,
ANDROID_VR.clientName,
ANDROID_VR.deviceModel,
ANDROID_VR.osVersion,
ANDROID_VR.userAgent,
ANDROID_VR.androidSdkVersion,
ANDROID_VR.clientVersion,
ANDROID_VR.canLogin,
false);
private static boolean forceAVC() { private static boolean forceAVC() {
return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get(); return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();

View File

@ -22,14 +22,6 @@ public class SpoofVideoStreamsPatch {
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point. Used by YT Music to disable stable volume.
*/
public static void setClientTypeToAndroidVrNoHl() {
Logger.printDebug(() -> "Setting stream spoofing to: " + ClientType.ANDROID_VR_NO_HL);
BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.save(ClientType.ANDROID_VR_NO_HL);
}
/** /**
* Injection point. * Injection point.
* Blocks /get_watch requests by returning an unreachable URI. * Blocks /get_watch requests by returning an unreachable URI.
@ -173,10 +165,11 @@ public class SpoofVideoStreamsPatch {
return postData; return postData;
} }
public static final class ForceiOSAVCAvailability implements Setting.Availability { public static final class SpoofiOSAvailability implements Setting.Availability {
@Override @Override
public boolean isAvailable() { public boolean isAvailable() {
return BaseSettings.SPOOF_VIDEO_STREAMS.get() && BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS; return BaseSettings.SPOOF_VIDEO_STREAMS.get()
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS;
} }
} }
} }

View File

@ -19,7 +19,9 @@ final class PlayerRoutes {
"?fields=streamingData" + "?fields=streamingData" +
"&alt=proto" "&alt=proto"
).compile(); ).compile();
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
/** /**
* TCP connection and HTTP read timeout * TCP connection and HTTP read timeout
*/ */

View File

@ -0,0 +1,41 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ForceOriginalAudioPatch {
private static final String DEFAULT_AUDIO_TRACKS_IDENTIFIER = "original";
/**
* Injection point.
*/
public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) {
try {
if (!Settings.FORCE_ORIGINAL_AUDIO.get()) {
return isDefault;
}
if (audioTrackDisplayName.isEmpty()) {
// Older app targets can have empty audio tracks and these might be placeholders.
// The real audio tracks are called after these.
return isDefault;
}
Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: "
+ String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName);
final boolean isOriginal = audioTrackDisplayName.contains(DEFAULT_AUDIO_TRACKS_IDENTIFIER);
if (isOriginal) {
Logger.printDebug(() -> "Using audio: " + audioTrackId);
}
return isOriginal;
} catch (Exception ex) {
Logger.printException(() -> "isDefaultAudioStream failure", ex);
}
return isDefault;
}
}

View File

@ -52,6 +52,8 @@ public class Settings extends BaseSettings {
public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", -2.0f); public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", -2.0f);
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds",
"0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true); "0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true);
// Audio
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE);
// Ads // Ads
public static final BooleanSetting HIDE_BUTTONED_ADS = new BooleanSetting("revanced_hide_buttoned_ads", TRUE); public static final BooleanSetting HIDE_BUTTONED_ADS = new BooleanSetting("revanced_hide_buttoned_ads", TRUE);

View File

@ -48,35 +48,44 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
/** /**
* Sorts a preference list by menu entries, but preserves the first value as the first entry. * Sorts a preference list by menu entries, but preserves the first value as the first entry.
*
* @noinspection SameParameterValue
*/ */
private static void sortListPreferenceByValues(ListPreference listPreference) { private static void sortListPreferenceByValues(ListPreference listPreference, int firstEntriesToPreserve) {
CharSequence[] entries = listPreference.getEntries(); CharSequence[] entries = listPreference.getEntries();
CharSequence[] entryValues = listPreference.getEntryValues(); CharSequence[] entryValues = listPreference.getEntryValues();
final int entrySize = entries.length; final int entrySize = entries.length;
if (entrySize != entryValues.length) { if (entrySize != entryValues.length) {
// Xml array declaration has a missing/extra entry.
throw new IllegalStateException(); throw new IllegalStateException();
} }
// Ensure the first entry remains the first after sorting. List<Pair<String, String>> firstPairs = new ArrayList<>(firstEntriesToPreserve);
CharSequence firstEntry = entries[0]; List<Pair<String, String>> pairsToSort = new ArrayList<>(entrySize);
CharSequence firstEntryValue = entryValues[0];
List<Pair<String, String>> entryPairs = new ArrayList<>(entrySize); for (int i = 0; i < entrySize; i++) {
for (int i = 1; i < entrySize; i++) { Pair<String, String> pair = new Pair<>(entries[i].toString(), entryValues[i].toString());
entryPairs.add(new Pair<>(entries[i].toString(), entryValues[i].toString())); if (i < firstEntriesToPreserve) {
firstPairs.add(pair);
} else {
pairsToSort.add(pair);
}
} }
Collections.sort(entryPairs, (pair1, pair2) -> pair1.first.compareToIgnoreCase(pair2.first)); Collections.sort(pairsToSort, (pair1, pair2) -> pair1.first.compareToIgnoreCase(pair2.first));
CharSequence[] sortedEntries = new CharSequence[entrySize]; CharSequence[] sortedEntries = new CharSequence[entrySize];
CharSequence[] sortedEntryValues = new CharSequence[entrySize]; CharSequence[] sortedEntryValues = new CharSequence[entrySize];
sortedEntries[0] = firstEntry; int i = 0;
sortedEntryValues[0] = firstEntryValue; for (Pair<String, String> pair : firstPairs) {
sortedEntries[i] = pair.first;
sortedEntryValues[i] = pair.second;
i++;
}
int i = 1; for (Pair<String, String> pair : pairsToSort) {
for (Pair<String, String> pair : entryPairs) {
sortedEntries[i] = pair.first; sortedEntries[i] = pair.first;
sortedEntryValues[i] = pair.second; sortedEntryValues[i] = pair.second;
i++; i++;
@ -102,7 +111,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
preference = findPreference(Settings.SPOOF_VIDEO_STREAMS_LANGUAGE.key); preference = findPreference(Settings.SPOOF_VIDEO_STREAMS_LANGUAGE.key);
if (preference instanceof ListPreference languagePreference) { if (preference instanceof ListPreference languagePreference) {
sortListPreferenceByValues(languagePreference); sortListPreferenceByValues(languagePreference, 1);
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex); Logger.printException(() -> "initialize failure", ex);

View File

@ -1392,6 +1392,10 @@ public final class app/revanced/patches/youtube/shared/FingerprintsKt {
public static final fun getRollingNumberTextViewAnimationUpdateFingerprint ()Lapp/revanced/patcher/Fingerprint; public static final fun getRollingNumberTextViewAnimationUpdateFingerprint ()Lapp/revanced/patcher/Fingerprint;
} }
public final class app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatchKt {
public static final fun getForceOriginalAudioPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/video/information/VideoInformationPatchKt { public final class app/revanced/patches/youtube/video/information/VideoInformationPatchKt {
public static final fun getVideoInformationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getVideoInformationPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
public static final fun userSelectedPlaybackSpeedHook (Ljava/lang/String;Ljava/lang/String;)V public static final fun userSelectedPlaybackSpeedHook (Ljava/lang/String;Ljava/lang/String;)V

View File

@ -1,15 +1,7 @@
package app.revanced.patches.music.misc.spoof package app.revanced.patches.music.misc.spoof
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patches.music.misc.gms.musicActivityOnCreateFingerprint
import app.revanced.patches.shared.misc.spoof.EXTENSION_CLASS_DESCRIPTOR
import app.revanced.patches.shared.misc.spoof.spoofVideoStreamsPatch import app.revanced.patches.shared.misc.spoof.spoofVideoStreamsPatch
val spoofVideoStreamsPatch = spoofVideoStreamsPatch({ val spoofVideoStreamsPatch = spoofVideoStreamsPatch({
compatibleWith("com.google.android.apps.youtube.music") compatibleWith("com.google.android.apps.youtube.music")
}, {
musicActivityOnCreateFingerprint.method.addInstruction(
0,
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setClientTypeToAndroidVrNoHl()V"
)
}) })

View File

@ -110,7 +110,6 @@ val enableDebuggingPatch = bytecodePatch(
""" """
) )
} }
}
experimentalStringFeatureFlagFingerprint.match( experimentalStringFeatureFlagFingerprint.match(
experimentalFeatureFlagParentFingerprint.originalClassDef experimentalFeatureFlagParentFingerprint.originalClassDef
@ -127,6 +126,7 @@ val enableDebuggingPatch = bytecodePatch(
""" """
) )
} }
}
// There exists other experimental accessor methods for byte[] // There exists other experimental accessor methods for byte[]
// and wrappers for obfuscated classes, but currently none of those are hooked. // and wrappers for obfuscated classes, but currently none of those are hooked.

View File

@ -0,0 +1,23 @@
package app.revanced.patches.youtube.video.audio
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
internal val streamingModelBuilderFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("L")
strings("vprng")
}
internal val menuItemAudioTrackFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
parameters("L")
returns("V")
strings("menu_item_audio_track")
}
internal val audioStreamingTypeSelector = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL)
returns("L")
strings("raw") // String is not unique
}

View File

@ -0,0 +1,159 @@
package app.revanced.patches.youtube.video.audio
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.immutable.ImmutableField
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/ForceOriginalAudioPatch;"
@Suppress("unused")
val forceOriginalAudioPatch = bytecodePatch(
name = "Force original audio",
description = "Adds an option to always use the original audio track.",
) {
dependsOn(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
)
compatibleWith(
"com.google.android.youtube"(
"18.38.44",
"18.49.37",
"19.16.39",
"19.25.37",
"19.34.42",
"19.43.41",
"19.45.38",
"19.46.42",
),
)
execute {
addResources("youtube", "video.audio.forceOriginalAudioPatch")
PreferenceScreen.VIDEO.addPreferences(
SwitchPreference("revanced_force_original_audio")
)
fun Method.firstFormatStreamingModelCall(
returnType: String = "Ljava/lang/String;"
): MutableMethod {
val audioTrackIdIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"
&& reference.returnType == returnType
}
return navigate(this).to(audioTrackIdIndex).stop()
}
// Accessor methods of FormatStreamModel have no string constants and
// opcodes are identical to other methods in the same class,
// so must walk from another class that use the methods.
val isDefaultMethod = streamingModelBuilderFingerprint.originalMethod.firstFormatStreamingModelCall("Z")
val audioTrackIdMethod = menuItemAudioTrackFingerprint.originalMethod.firstFormatStreamingModelCall()
val audioTrackDisplayNameMethod = audioStreamingTypeSelector.originalMethod.firstFormatStreamingModelCall()
val formatStreamModelClass = proxy(classes.first {
it.type == audioTrackIdMethod.definingClass
}).mutableClass
formatStreamModelClass.apply {
// Add a new field to store the override.
val helperFieldName = "isDefaultAudioTrackOverride"
fields.add(
ImmutableField(
type,
helperFieldName,
"Ljava/lang/Boolean;",
// Boolean is a 100% immutable class (all fields are final)
// and safe to write to a shared field without volatile/synchronization,
// but without volatile the field can show stale data
// and the same field is calculated more than once by different threads.
AccessFlags.PRIVATE.value or AccessFlags.VOLATILE.value,
null,
null,
null
).toMutable()
)
// Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed.
val helperMethodClass = type
val helperMethodName = "extension_isDefaultAudioTrack"
val helperMethod = ImmutableMethod(
helperMethodClass,
helperMethodName,
listOf(ImmutableMethodParameter("Z", null, null)),
"Z",
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(6),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
if-eqz v0, :call_extension
invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z
move-result v3
return v3
:call_extension
invoke-virtual { p0 }, $audioTrackIdMethod
move-result-object v1
invoke-virtual { p0 }, $audioTrackDisplayNameMethod
move-result-object v2
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z
move-result v3
invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
move-result-object v0
iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
return v3
"""
)
}
methods.add(helperMethod)
// Modify isDefaultAudioTrack() to call extension helper method.
isDefaultMethod.apply {
val index = indexOfFirstInstructionOrThrow(Opcode.RETURN)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructions(
index,
"""
invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z
move-result v$register
"""
)
}
}
}
}

View File

@ -1226,6 +1226,11 @@ Enabling this can unlock higher video qualities"</string>
<string name="revanced_disable_zoom_haptics_summary_on">Haptics are disabled</string> <string name="revanced_disable_zoom_haptics_summary_on">Haptics are disabled</string>
<string name="revanced_disable_zoom_haptics_summary_off">Haptics are enabled</string> <string name="revanced_disable_zoom_haptics_summary_off">Haptics are enabled</string>
</patch> </patch>
<patch id="video.audio.forceOriginalAudioPatch">
<string name="revanced_force_original_audio_title">Forced original audio</string>
<string name="revanced_force_original_audio_summary_on">Using original audio</string>
<string name="revanced_force_original_audio_summary_off">Using default audio</string>
</patch>
<patch id="video.quality.rememberVideoQualityPatch"> <patch id="video.quality.rememberVideoQualityPatch">
<!-- Translations should use the same text as revanced_custom_playback_speeds_auto --> <!-- Translations should use the same text as revanced_custom_playback_speeds_auto -->
<string name="revanced_video_quality_default_entry_1">Auto</string> <string name="revanced_video_quality_default_entry_1">Auto</string>
@ -1292,8 +1297,8 @@ AVC has a maximum resolution of 1080p, Opus audio codec is not available, and vi
• Videos end 1 second early"</string> • Videos end 1 second early"</string>
<string name="revanced_spoof_video_streams_about_android_vr_title">Android VR spoofing side effects</string> <string name="revanced_spoof_video_streams_about_android_vr_title">Android VR spoofing side effects</string>
<string name="revanced_spoof_video_streams_about_android_vr_summary">"• Kids videos may not play <string name="revanced_spoof_video_streams_about_android_vr_summary">"• Kids videos may not play
Livestreams start from the beginning Audio track menu is missing
Videos end 1 second early"</string> Stable volume is not available"</string>
<string name="revanced_spoof_video_streams_language_title">Default audio stream language</string> <string name="revanced_spoof_video_streams_language_title">Default audio stream language</string>
<string name="revanced_spoof_video_streams_language_DEFAULT">App language</string> <string name="revanced_spoof_video_streams_language_DEFAULT">App language</string>
<string name="revanced_spoof_video_streams_language_AR">Arabic</string> <string name="revanced_spoof_video_streams_language_AR">Arabic</string>