fix(YouTube): Update fingerprints and logic for YouTube 21.34, add custom settings
Release / Release (push) Failing after 16m39s

Reworks fingerprint matching across ads, GMS core, litho filter, player
controls, context hook, background playback, end screen cards, miniplayer,
and Shorts autoplay patches to keep working on YouTube 21.34, where several
underlying methods and resources changed. Also fixes video stream spoofing
to build the request URI via Uri.Builder instead of the removed toString()
path, and adds a new Custom settings screen with AMOLED/pure black player,
ambient mode, Shorts hiding, playback speed/quality defaults, debug video
info, custom app name, startup tab, and URL cleaning options.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NXRsEcWGqDhsCbfEdrfCdE
This commit is contained in:
whitehai11
2026-09-03 19:50:50 +02:00
co-authored by Claude Sonnet 5
parent 7427432759
commit 97b4fc5b5a
19 changed files with 547 additions and 221 deletions
@@ -236,6 +236,17 @@ public final class AdsFilter extends Filter {
return Settings.HIDE_GENERAL_ADS.get();
}
/**
* Injection point.
* 21.34+ the player overlay timely shelf handler no longer gets a dedicated method,
* so instead the result of the overlay type `String.equals` comparison is passed in.
*
* @param original Whether the overlay is the timely shelf (already being shown when not hiding ads).
*/
public static boolean allowAds(boolean original) {
return Settings.HIDE_GENERAL_ADS.get() ? false : original;
}
/**
* Injection point.
*/
@@ -31,14 +31,11 @@ internal val BytecodePatchContext.buildInitPlaybackRequestMethodMatch by composi
}
internal val BytecodePatchContext.buildPlayerRequestURIMethodMatch by composingFirstMethod("key", "asig") {
returnType("Ljava/lang/String;")
returnType($$"Landroid/net/Uri$Builder;")
opcodes(
Opcode.INVOKE_VIRTUAL, // Register holds player request URI.
Opcode.INVOKE_VIRTUAL, // The last Uri$Builder mutating call. Register holds the player request Uri$Builder.
Opcode.MOVE_RESULT_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.IGET_OBJECT,
Opcode.MONITOR_EXIT,
Opcode.RETURN_OBJECT,
Opcode.RETURN_OBJECT, // Returns the built player request Uri$Builder.
)
}
@@ -154,7 +151,7 @@ internal val BytecodePatchContext.nerdsStatsVideoFormatBuilderMethod by gettingF
returnType("Ljava/lang/String;")
parameterTypes("L")
instructions(
"codecs=\""(),
", codecs="(),
)
}
@@ -82,14 +82,19 @@ internal fun spoofVideoStreamsPatch(
// region Block /get_watch requests to fall back to /player requests.
buildPlayerRequestURIMethodMatch.method.apply {
val invokeToStringIndex = buildPlayerRequestURIMethodMatch[0]
val uriRegister = getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC
val returnIndex = buildPlayerRequestURIMethodMatch[-1]
val builderRegister = getInstruction<OneRegisterInstruction>(returnIndex).registerA
val freeRegister = findFreeRegister(returnIndex, builderRegister)
addInstructions(
invokeToStringIndex,
"""
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
move-result-object v$uriRegister
returnIndex,
$$"""
invoke-virtual { v$builderRegister }, Landroid/net/Uri$Builder;->build()Landroid/net/Uri;
move-result-object v$freeRegister
invoke-static { v$freeRegister }, $$EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
move-result-object v$freeRegister
invoke-virtual { v$freeRegister }, Landroid/net/Uri;->buildUpon()Landroid/net/Uri$Builder;
move-result-object v$builderRegister
""",
)
}
@@ -71,3 +71,27 @@ internal val BytecodePatchContext.playerOverlayTimelyShelfMethod by gettingFirst
)
}
/**
* 21.34+ removed the [String]"innertube_cue_range", "Null id" and "Null onExitActions" strings
* from the player overlay timely shelf handler, and the handler moved into a synthetic lambda
* (`a(Ljava/lang/Object;)V`) that dispatches on the overlay type via a `String.equals` comparison.
* Only [String]"player_overlay_timely_shelf" remains, so match on it plus the following
* `String.equals`/`move-result` used to check if the overlay is the timely shelf.
*/
internal val BytecodePatchContext.playerOverlayTimelyShelfMethod21_34 by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
parameterTypes("Ljava/lang/Object;")
instructions(
"player_overlay_timely_shelf"(),
afterAtMost(
5,
allOf(
Opcode.INVOKE_VIRTUAL(),
method { name == "equals" && definingClass == "Ljava/lang/String;" },
),
),
after(Opcode.MOVE_RESULT()),
)
}
@@ -24,6 +24,7 @@ import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch
import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
import app.revanced.patches.youtube.misc.playservice.is_20_14_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_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
@@ -34,6 +35,7 @@ import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.injectHideViewCall
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
private const val EXTENSION_CLASS_DESCRIPTOR =
@@ -94,7 +96,8 @@ val hideAdsPatch = bytecodePatch(
"20.26.46",
"20.31.42",
"20.37.48",
"20.40.45"
"20.40.45",
"21.34.248"
),
)
@@ -154,17 +157,41 @@ val hideAdsPatch = bytecodePatch(
// Hide player overlay view. This can be hidden with a regular litho filter
// but an empty space remains.
if (is_20_14_or_greater) {
playerOverlayTimelyShelfMethod.addInstructionsWithLabels(
0,
"""
invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z
move-result v0
if-eqz v0, :show
return-void
:show
nop
"""
)
if (is_21_34_or_greater) {
// 21.34+ the overlay handler moved into a synthetic lambda `a(Object)V`
// that dispatches on overlay type. Hook right after the `String.equals` /
// `move-result` that determines the overlay is the timely shelf so that
// only the timely shelf is hidden and not every player overlay.
playerOverlayTimelyShelfMethod21_34.let { match ->
val insertIndex = match[-1]
val equalsResultRegister =
match.method.getInstruction<OneRegisterInstruction>(insertIndex).registerA
match.method.addInstructionsWithLabels(
insertIndex + 1,
"""
invoke-static { v$equalsResultRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->allowAds(Z)Z
move-result v$equalsResultRegister
if-nez v$equalsResultRegister, :show
return-void
:show
nop
""",
)
}
} else {
playerOverlayTimelyShelfMethod.addInstructionsWithLabels(
0,
"""
invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z
move-result v0
if-eqz v0, :show
return-void
:show
nop
"""
)
}
}
@@ -13,6 +13,7 @@ import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_19_43_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_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
@@ -68,7 +69,8 @@ val hideEndScreenCardsPatch = bytecodePatch(
"20.26.46",
"20.31.42",
"20.37.48",
"20.40.45"
"20.40.45",
"21.34.248"
),
)
@@ -90,7 +92,11 @@ val hideEndScreenCardsPatch = bytecodePatch(
}
}
if (is_19_43_or_greater) {
// 21.34+ the end screen cards are no longer shown through a dedicated 5-method
// container class that the legacy showEndscreenCardsMethod fingerprint can locate.
// Instead each card view (circle/icon/video) is created and hidden individually by the
// three layout fingerprints hooked above, which cover hiding the whole card on 21.34.
if (is_19_43_or_greater && !is_21_34_or_greater) {
showEndscreenCardsMethod.addInstructionsWithLabels(
0,
"""
@@ -92,8 +92,15 @@ val miniplayerPatch = bytecodePatch(
val preferences = mutableSetOf<BasePreference>()
preferences +=
if (is_20_37_or_greater) {
if (is_20_37_or_greater && !is_21_34_or_greater) {
ListPreference("revanced_miniplayer_type")
} else if (is_21_34_or_greater) {
// YouTube 21.29+ removed all modern miniplayers except "Modern 4",
// so the type picker no longer has any effect.
NonInteractivePreference(
key = "revanced_miniplayer_type",
summaryKey = "revanced_miniplayer_type_not_available_summary",
)
} else if (is_20_03_or_greater) {
ListPreference(
key = "revanced_miniplayer_type",
@@ -153,7 +160,9 @@ val miniplayerPatch = bytecodePatch(
)
}
preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER)
if (!is_21_34_or_greater) {
preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER)
}
PreferenceScreen.PLAYER.addPreferences(
PreferenceScreenPreference(
@@ -265,101 +274,113 @@ val miniplayerPatch = bytecodePatch(
// region Enable modern miniplayer.
miniplayerModernConstructorMethod.classDef.methods.forEach {
it.apply {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
val iPutIndex = indexOfFirstInstructionOrThrow {
this.opcode == Opcode.IPUT && this.getReference<FieldReference>()?.type == "I"
}
// Select which modern miniplayer type is used.
// YouTube 21.29+ removed all modern miniplayers except "Modern 4", so there is
// nothing to choose between and this override is dead code.
if (!is_21_34_or_greater) {
miniplayerModernConstructorMethod.classDef.methods.forEach {
it.apply {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
val iPutIndex = indexOfFirstInstructionOrThrow {
this.opcode == Opcode.IPUT && this.getReference<FieldReference>()?.type == "I"
}
insertModernMiniplayerTypeOverride(iPutIndex)
} else {
findReturnIndicesReversed().forEach { index ->
insertModernMiniplayerOverride(
index
)
insertModernMiniplayerTypeOverride(iPutIndex)
} else {
findReturnIndicesReversed().forEach { index ->
insertModernMiniplayerOverride(
index
)
}
}
}
}
}
if (is_19_23_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DRAG_DROP_FEATURE_KEY,
"getMiniplayerDragAndDrop",
)
}
if (is_19_25_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_MODERN_FEATURE_LEGACY_KEY,
"getModernMiniplayerOverride",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_MODERN_FEATURE_KEY,
"getModernFeatureFlagsActiveOverride",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DOUBLE_TAP_FEATURE_KEY,
"getMiniplayerDoubleTapAction",
)
}
if (is_19_26_or_greater) {
miniplayerModernConstructorMethod.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(
MINIPLAYER_INITIAL_SIZE_FEATURE_KEY,
)
val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT)
val register = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
targetIndex + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getMiniplayerDefaultSize(I)I
move-result v$register
""",
// Modern miniplayer feature-flag overrides.
// YouTube 21.29+ removed all modern miniplayers except "Modern 4" and moved/removed the
// feature flags for drag & drop, double-tap, rounded corners, horizontal drag and animated
// expand. The default/minimum size flags were removed in 21.32+. The fingerprints below
// therefore no longer resolve on 21.34+, so these overrides only apply to older versions.
if (!is_21_34_or_greater) {
if (is_19_23_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DRAG_DROP_FEATURE_KEY,
"getMiniplayerDragAndDrop",
)
}
// Override a minimum size constant.
miniplayerMinimumSizeMethodMatch.let {
it.method.apply {
val index = it[1]
val register = getInstruction<OneRegisterInstruction>(index).registerA
if (is_19_25_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_MODERN_FEATURE_LEGACY_KEY,
"getModernMiniplayerOverride",
)
// Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller.
// The 170 initial limit probably could be patched to allow even smaller initial sizes,
// but 170 is already half the horizontal space and smaller does not seem useful.
replaceInstruction(index, "const/16 v$register, 170")
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_MODERN_FEATURE_KEY,
"getModernFeatureFlagsActiveOverride",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DOUBLE_TAP_FEATURE_KEY,
"getMiniplayerDoubleTapAction",
)
}
if (is_19_26_or_greater) {
miniplayerModernConstructorMethod.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(
MINIPLAYER_INITIAL_SIZE_FEATURE_KEY,
)
val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT)
val register = getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
targetIndex + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getMiniplayerDefaultSize(I)I
move-result v$register
""",
)
}
// Override a minimum size constant.
miniplayerMinimumSizeMethodMatch.let {
it.method.apply {
val index = it[1]
val register = getInstruction<OneRegisterInstruction>(index).registerA
// Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller.
// The 170 initial limit probably could be patched to allow even smaller initial sizes,
// but 170 is already half the horizontal space and smaller does not seem useful.
replaceInstruction(index, "const/16 v$register, 170")
}
}
}
}
if (is_19_36_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY,
"getRoundedCorners",
)
}
if (is_19_36_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY,
"getRoundedCorners",
)
}
if (is_19_43_or_greater) {
miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DISABLED_FEATURE_KEY,
"getMiniplayerOnCloseHandler",
)
if (is_19_43_or_greater) {
miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DISABLED_FEATURE_KEY,
"getMiniplayerOnCloseHandler",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY,
"getHorizontalDrag",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY,
"getHorizontalDrag",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY,
"getMaximizeAnimation",
)
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY,
"getMaximizeAnimation",
)
}
}
// endregion
@@ -391,7 +412,8 @@ val miniplayerPatch = bytecodePatch(
// region fix minimal miniplayer using the wrong pause/play bold icons.
if (is_20_31_or_greater) {
// 21.34 does not have the yt_fill_pause_* drawables this fingerprint anchors on.
if (is_20_31_or_greater && !is_21_34_or_greater) {
miniplayerSetIconsMethod.apply {
findInstructionIndicesReversedOrThrow {
val reference = getReference<MethodReference>()
@@ -421,7 +443,6 @@ val miniplayerPatch = bytecodePatch(
ClassDef::miniplayerModernActionButtonMethodMatch to "hideMiniplayerActionButton",
ClassDef::miniplayerModernRewindButtonMethodMatch to "hideMiniplayerRewindForward",
ClassDef::miniplayerModernForwardButtonMethodMatch to "hideMiniplayerRewindForward",
ClassDef::miniplayerModernOverlayViewMethodMatch to "adjustMiniplayerOpacity",
).forEach { (matching, methodName) ->
val match = matching.get(miniplayerModernViewParentMethod.immutableClassDef)
@@ -436,6 +457,23 @@ val miniplayerPatch = bytecodePatch(
}
}
// Adjust the overlay opacity. The modern overlay was restructured in 21.29+ alongside the
// removal of the default/minimum size flags, so this hook is only applied on older versions.
if (!is_21_34_or_greater) {
val match = ClassDef::miniplayerModernOverlayViewMethodMatch
.get(miniplayerModernViewParentMethod.immutableClassDef)
match.method.apply {
val index = match[-1]
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstruction(
index + 1,
"invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->adjustMiniplayerOpacity(Landroid/view/View;)V",
)
}
}
miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernAddViewListenerMethod()
.addInstruction(
0,
@@ -15,6 +15,7 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_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
@@ -93,103 +94,109 @@ val shortsAutoplayPatch = bytecodePatch(
)
}
val reelPlaybackRepeatMethod =
reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod()
reelPlaybackRepeatMethod.apply {
// The behavior enums are looked up from an ordinal value to an enum type.
findInstructionIndicesReversedOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == reelEnumClass &&
reference.parameterTypes.firstOrNull() == "I" &&
reference.returnType == reelEnumClass
}.forEach { index ->
val register = getInstruction<OneRegisterInstruction>(index + 1).registerA
addInstructions(
index + 2,
"""
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum;
move-result-object v$register
""",
)
}
}
// As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method.
// Manually restore the removed 'Autoplay' code.
if (is_20_09_or_greater) {
// Variable names are only a rough guess of what these methods do.
val userActionMethodReference =
reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[1]).methodReference!!
val reelSequenceControllerMethodReference =
reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[2]).methodReference!!
// YouTube 21.29+ replaced the diagnostics string and reworked the reel playback repeat
// method (its signature changed from "V(L)" to "bL(JLbetu;J)V"), so the parent fingerprint
// below no longer resolves on 21.34+. The autoplay restore logic thus only applies to
// versions below 21.34.
if (!is_21_34_or_greater) {
val reelPlaybackRepeatMethod =
reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod()
reelPlaybackRepeatMethod.apply {
// Find the first call modified by extension code above.
val extensionReturnResultIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_STATIC &&
getReference<MethodReference>()?.definingClass == EXTENSION_CLASS_DESCRIPTOR
} + 1
val enumRegister =
getInstruction<OneRegisterInstruction>(extensionReturnResultIndex).registerA
val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.definingClass == definingClass &&
reference.type == reelSequenceControllerMethodReference.definingClass
}
val getReelSequenceControllerReference =
getInstruction<ReferenceInstruction>(getReelSequenceControllerIndex).reference
// The behavior enums are looked up from an ordinal value to an enum type.
findInstructionIndicesReversedOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == reelEnumClass &&
reference.parameterTypes.firstOrNull() == "I" &&
reference.returnType == reelEnumClass
}.forEach { index ->
val register = getInstruction<OneRegisterInstruction>(index + 1).registerA
// Add a helper method to avoid finding multiple free registers.
// If enum is autoplay then method performs autoplay and returns null,
// otherwise returns the same enum.
val helperClass = definingClass
val helperName = "patch_handleAutoPlay"
val helperReturnType = "Ljava/lang/Enum;"
val helperMethod = ImmutableMethod(
helperClass,
helperName,
listOf(ImmutableMethodParameter("Ljava/lang/Enum;", null, null)),
helperReturnType,
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(7),
).toMutable().apply {
addInstructionsWithLabels(
0,
addInstructions(
index + 2,
"""
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z
move-result v0
if-eqz v0, :ignore
new-instance v0, ${userActionMethodReference.definingClass}
const/4 v1, 0x3
const/4 v2, 0x0
invoke-direct { v0, v1, v2, v2 }, $userActionMethodReference
iget-object v3, p0, $getReelSequenceControllerReference
invoke-virtual { v3, v0 }, $reelSequenceControllerMethodReference
const/4 v4, 0x0
return-object v4
:ignore
return-object p1
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum;
move-result-object v$register
""",
)
}
reelPlaybackRepeatMethod.classDef.methods.add(helperMethod)
}
addInstructionsWithLabels(
extensionReturnResultIndex + 1,
"""
invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType
move-result-object v$enumRegister
if-nez v$enumRegister, :ignore
return-void # Autoplay was performed.
:ignore
nop
""",
)
// As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method.
// Manually restore the removed 'Autoplay' code.
if (is_20_09_or_greater) {
// Variable names are only a rough guess of what these methods do.
val userActionMethodReference =
reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[1]).methodReference!!
val reelSequenceControllerMethodReference =
reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[2]).methodReference!!
reelPlaybackRepeatMethod.apply {
// Find the first call modified by extension code above.
val extensionReturnResultIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_STATIC &&
getReference<MethodReference>()?.definingClass == EXTENSION_CLASS_DESCRIPTOR
} + 1
val enumRegister =
getInstruction<OneRegisterInstruction>(extensionReturnResultIndex).registerA
val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.definingClass == definingClass &&
reference.type == reelSequenceControllerMethodReference.definingClass
}
val getReelSequenceControllerReference =
getInstruction<ReferenceInstruction>(getReelSequenceControllerIndex).reference
// Add a helper method to avoid finding multiple free registers.
// If enum is autoplay then method performs autoplay and returns null,
// otherwise returns the same enum.
val helperClass = definingClass
val helperName = "patch_handleAutoPlay"
val helperReturnType = "Ljava/lang/Enum;"
val helperMethod = ImmutableMethod(
helperClass,
helperName,
listOf(ImmutableMethodParameter("Ljava/lang/Enum;", null, null)),
helperReturnType,
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(7),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z
move-result v0
if-eqz v0, :ignore
new-instance v0, ${userActionMethodReference.definingClass}
const/4 v1, 0x3
const/4 v2, 0x0
invoke-direct { v0, v1, v2, v2 }, $userActionMethodReference
iget-object v3, p0, $getReelSequenceControllerReference
invoke-virtual { v3, v0 }, $reelSequenceControllerMethodReference
const/4 v4, 0x0
return-object v4
:ignore
return-object p1
""",
)
}
reelPlaybackRepeatMethod.classDef.methods.add(helperMethod)
addInstructionsWithLabels(
extensionReturnResultIndex + 1,
"""
invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType
move-result-object v$enumRegister
if-nez v$enumRegister, :ignore
return-void # Autoplay was performed.
:ignore
nop
""",
)
}
}
}
}
@@ -12,6 +12,7 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_29_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_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
@@ -103,7 +104,10 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch(
kidsBackgroundPlaybackPolicyControllerMethod.returnEarly()
// Fix PiP buttons not working after locking/unlocking device screen.
if (is_19_34_or_greater) {
// YouTube 21.34+ removed the flag this anchors on (the remaining case is an
// unfixable Google-side issue, see RVX #2764). Skipping it avoids the
// fingerprint being unresolved and crashing the whole patch.
if (is_19_34_or_greater && !is_21_34_or_greater) {
pipInputConsumerFeatureFlagMethodMatch.let {
it.method.insertLiteralOverride(
it[0],
@@ -15,6 +15,7 @@ import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.returnType
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.cloneMutableAndPreserveParameters
import app.revanced.util.findInstructionIndicesReversedOrThrow
@@ -73,12 +74,25 @@ val hookClientContextPatch = bytecodePatch(
}
}
authenticationChangeListenerMethod.apply {
val messageLiteBuilderIndex =
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass)
// The Authentication changed listener method (where the message lite builder method was
// sourced from) was removed in YouTube 21.34+. Derive the builder method from the dummy
// client context body method instead, which still resolves and builds the same body.
if (!is_21_34_or_greater) {
authenticationChangeListenerMethod.apply {
val messageLiteBuilderIndex =
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass)
messageLiteBuilderMethod =
getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!!
messageLiteBuilderMethod =
getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!!
}
} else {
buildDummyClientContextBodyMethodMatch.method.apply {
val messageLiteBuilderIndex =
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass)
messageLiteBuilderMethod =
getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!!
}
}
buildClientContextBodyConstructorMethod.immutableClassDef.buildClientContextBodyMethodMatch.let {
@@ -11,7 +11,6 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch
returnType("V")
parameterTypes()
instructions(
ResourceType.DRAWABLE("ic_offline_no_content_upside_down"),
ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"),
method { name == "getString" && returnType == "Ljava/lang/String;" },
after(Opcode.MOVE_RESULT_OBJECT()),
@@ -20,12 +19,13 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch
// It's not clear if this second class is ever used, and it may be dead code,
// but it is the layout image/text is identical to the network error match above.
// The offline image drawable (ic_offline_no_content_upside_down) was removed in 21.34,
// so only the still-present offline_no_content string is matched.
internal val BytecodePatchContext.loadingFrameLayoutControllerMethodMatch by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
parameterTypes("L")
parameterTypes("Z")
instructions(
ResourceType.DRAWABLE("ic_offline_no_content_upside_down"),
ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"),
method { name == "getString" && returnType == "Ljava/lang/String;" },
after(Opcode.MOVE_RESULT_OBJECT()),
@@ -59,13 +59,18 @@ val lithoFilterPatch = lithoFilterPatch(
}
// Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf).
lithoConverterBufferUpbFeatureFlagMethodMatch.let {
// 20.22 the flag is still enabled in one location, but what it does is not known.
// Disable it anyway.
it.method.insertLiteralOverride(
it[0],
false,
)
// The flag (45419603) was removed in 21.34. In 21.34 the native UPB buffer path is the
// default (and required) path for the setProtoBuffer([B) hook, so nothing needs to be
// forced off.
if (!is_21_34_or_greater) {
lithoConverterBufferUpbFeatureFlagMethodMatch.let {
// 20.22 the flag is still enabled in one location, but what it does is not known.
// Disable it anyway.
it.method.insertLiteralOverride(
it[0],
false,
)
}
}
// endregion
}
@@ -34,6 +34,27 @@ internal val ClassDef.motionEventMethodMatch by ClassDefComposing.composingFirst
instructions(method("setTranslationY"))
}
// The player controls overlay was rebuilt in 21.34 and no longer calls `setTranslationY`.
// Instead the tap-to-dismiss handler is `nwo.b(Landroid/view/MotionEvent;)V`, which triggers the
// visibility change via `nwo.U()V`. These two fingerprints resolve that new hook and keep the
// `visibilityNegatedImmediate` injection working for the newer layout.
internal val BytecodePatchContext.youtubeControlsOverlayMethod21_34 by gettingFirstImmutableMethodDeclaratively {
returnType("V")
parameterTypes("Landroid/view/MotionEvent;")
instructions(
field { definingClass == "Lnwo;" && name == "br" },
method { definingClass == "Lnwo;" && name == "U" },
)
}
internal val ClassDef.motionEventMethodMatch21_34 by ClassDefComposing.composingFirstMethod {
returnType("V")
parameterTypes("Landroid/view/MotionEvent;")
instructions(
method { definingClass == "Lnwo;" && name == "U" },
)
}
internal val BytecodePatchContext.playerControlsExtensionHookListenersExistMethod by gettingFirstMethodDeclaratively {
name("fullscreenButtonVisibilityCallbacksExist")
definingClass(EXTENSION_CLASS_DESCRIPTOR)
@@ -268,7 +268,13 @@ val playerControlsPatch = bytecodePatch(
visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistMethod
visibilityImmediateMethod = playerControlsExtensionHookMethod
youtubeControlsOverlayMethod.immutableClassDef.motionEventMethodMatch.let {
val motionEventMethodMatch = if (is_21_34_or_greater) {
youtubeControlsOverlayMethod21_34.immutableClassDef.motionEventMethodMatch21_34
} else {
youtubeControlsOverlayMethod.immutableClassDef.motionEventMethodMatch
}
motionEventMethodMatch.let {
visibilityNegatedImmediateMethod = it.method
visibilityNegatedImmediateInsertIndex = it[0] + 1
}
@@ -127,6 +127,8 @@ var is_21_07_or_greater : Boolean by Delegates.notNull()
private set
var is_21_08_or_greater : Boolean by Delegates.notNull()
private set
var is_21_34_or_greater: Boolean by Delegates.notNull()
private set
val versionCheckPatch = resourcePatch(
description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.",
@@ -184,5 +186,6 @@ val versionCheckPatch = resourcePatch(
is_21_06_or_greater = 260705000 <= playStoreServicesVersion
is_21_07_or_greater = 260805000 <= playStoreServicesVersion
is_21_08_or_greater = 260905000 <= playStoreServicesVersion
is_21_34_or_greater = 263410000 <= playStoreServicesVersion
}
}
@@ -21,6 +21,27 @@ internal val BytecodePatchContext.setThemeMethod by gettingFirstMethodDeclarativ
instructions(ResourceType.STRING("app_theme_appearance_dark"))
}
/**
* YouTube 21.34+ appearance resolver `Latyq;->t()Lkll;`.
*
* Returns the theme appearance enum (whose ordinal 1 is dark), resolving between the
* `app_theme_appearance_*` prefs, the dark-mode flag and the system UI mode. Among all
* `public final ()Lkll` methods it is uniquely identified by reading `Lklm;->f` (the stored
* appearance value string): the sibling helpers `u()`/`v()` read `Lklm;->e`/the system UI mode
* instead, while `qyn` reads `Lklm;->f` but returns `V`.
*/
internal val BytecodePatchContext.setThemeMethod21_34 by gettingFirstMethodDeclaratively {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("Lkll;")
parameterTypes()
instructions(
allOf(
Opcode.IGET_OBJECT(),
field { name == "f" && type == "Ljava/lang/String;" && definingClass == "Lklm;" },
),
)
}
internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("Z")
@@ -29,3 +50,22 @@ internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFir
afterAtMost(10, Opcode.MOVE_RESULT()),
)
}
/**
* YouTube 21.34+ Cairo settings gate `Loqy;->run()V`.
*
* The legacy `public final ()Z` feature-flag gate no longer exists. In 21.34 the Cairo settings
* screen is selected by feature flag `0x2b6c3c4` inside `Loqy;->run()V`, which chooses between the
* legacy (`xml/settings_fragment`, 0x7f180027) and Cairo (`xml/settings_fragment_cairo`,
* 0x7f180028) settings screens. The `0x7f180028` literal only appears in this method, which keeps
* it unambiguous among the other `0x2b6c3c4` references.
*/
internal val BytecodePatchContext.cairoFragmentConfigMethodMatch21_34 by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
instructions(
45532100L(),
afterAtMost(10, Opcode.MOVE_RESULT()),
afterAtMost(10, 2132279336L()),
)
}
@@ -23,6 +23,7 @@ import app.revanced.patches.youtube.misc.fix.contentprovider.fixContentProviderP
import app.revanced.patches.youtube.misc.fix.playbackspeed.fixPlaybackSpeedWhilePlayingPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_31_or_greater
import app.revanced.patches.youtube.misc.playservice.is_21_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.util.*
import com.android.tools.smali.dexlib2.AccessFlags
@@ -214,6 +215,64 @@ val settingsPatch = bytecodePatch(
)
PreferenceScreen.MISC.addPreferences(
PreferenceCategory(
titleKey = "revanced_custom_title",
sorting = Sorting.UNSORTED,
tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory",
preferences = setOf(
NonInteractivePreference(
key = "revanced_custom_reset_title",
titleKey = "revanced_custom_reset_title",
summaryKey = "revanced_custom_reset_summary",
tag = "app.revanced.extension.shared.settings.preference.CustomSettingsResetPreference",
),
SwitchPreference(
"revanced_custom_force_amoled",
),
SwitchPreference(
"revanced_custom_pure_black_player",
),
SwitchPreference(
"revanced_custom_disable_ambient_mode",
),
SwitchPreference(
"revanced_custom_hide_shorts",
),
ListPreference(
key = "revanced_custom_default_playback_speed",
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference",
),
SwitchPreference(
"revanced_custom_remember_playback_speed",
),
ListPreference(
key = "revanced_custom_default_video_quality",
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference",
),
SwitchPreference(
"revanced_custom_remember_video_quality",
),
SwitchPreference(
"revanced_custom_force_highest_quality",
),
SwitchPreference(
"revanced_custom_debug_video_info",
),
TextPreference(
key = "revanced_custom_app_name",
titleKey = "revanced_custom_app_name_title",
inputType = InputType.TEXT_CAP_CHARACTERS,
tag = "app.revanced.extension.shared.settings.preference.TextInputPreference",
),
ListPreference(
key = "revanced_custom_startup_tab",
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference",
),
SwitchPreference(
"revanced_custom_clean_urls",
),
),
),
TextPreference(
key = null,
titleKey = "revanced_pref_import_export_title",
@@ -230,7 +289,7 @@ val settingsPatch = bytecodePatch(
// Update shared dark mode status based on YT theme.
// This is needed because YT allows forcing light/dark mode
// which then differs from the system dark mode status.
setThemeMethod.apply {
(if (is_21_34_or_greater) setThemeMethod21_34 else setThemeMethod).apply {
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructionsAtControlFlowLabel(
@@ -241,10 +300,12 @@ val settingsPatch = bytecodePatch(
}
// Add setting to force Cairo settings fragment on/off.
cairoFragmentConfigMethodMatch.method.insertLiteralOverride(
cairoFragmentConfigMethodMatch[0],
"$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z",
)
(if (is_21_34_or_greater) cairoFragmentConfigMethodMatch21_34 else cairoFragmentConfigMethodMatch).let {
it.method.insertLiteralOverride(
it[0],
"$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z",
)
}
// Bold icon resources are found starting in 20.23, but many YT icons are not bold.
// 20.31 is the first version that seems to have all the bold icons.
@@ -183,6 +183,32 @@ internal val BytecodePatchContext.playbackSpeedClassMethod by gettingFirstMethod
opcodes(Opcode.RETURN_OBJECT)
}
/**
* YouTube 21.34+ playback-speed bottom sheet fragment factory `Lpzy;->aW(Lce;Ljava/lang/String;)Lpzy;`.
*
* In 21.34 the `PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT` string moved into the large state-holder
* factory `Lhxz;->p()Ljava/lang/Object;` (which returns `Ljava/lang/Object;`, not the fragment type),
* so the legacy `playbackSpeedClassMethod` fingerprint no longer resolves. The fragment is instead
* obtained from its static factory, lookup by tag with fallback creation, which returns the exact
* fragment instance (`Lpzy;`). It is uniquely identified by the exact return type `Lpzy;` together
* with the `getSupportFragmentManager` -> `f(Ljava/lang/String;)L` lookup sequence.
*/
internal val BytecodePatchContext.playbackSpeedClassMethod21_34 by gettingFirstMethodDeclaratively {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returnType("Lpzy;")
parameterTypes("L", "Ljava/lang/String;")
instructions(
method("getSupportFragmentManager"),
after(Opcode.MOVE_RESULT_OBJECT()),
after(method {
returnType.startsWith("L") && parameterTypes.size == 1 && parameterTypes.first() == "Ljava/lang/String;"
}),
after(Opcode.MOVE_RESULT_OBJECT()),
after(Opcode.IF_EQZ()),
after(Opcode.CHECK_CAST()),
)
}
/**
* YouTube 20.19 and lower.
*/
@@ -1516,6 +1516,7 @@ Limitation: Using the back button on the toolbar may not work"</string>
<string name="revanced_miniplayer_type_entry_5">Modern 2</string>
<string name="revanced_miniplayer_type_entry_6">Modern 3</string>
<string name="revanced_miniplayer_type_entry_7">Modern 4</string>
<string name="revanced_miniplayer_type_not_available_summary">Selecting a miniplayer type is no longer available on your YouTube version.</string>
<string name="revanced_miniplayer_disable_rounded_corners_title">Disable rounded corners</string>
<string name="revanced_miniplayer_disable_rounded_corners_summary_on">Corners are square</string>
<string name="revanced_miniplayer_disable_rounded_corners_summary_off">Corners are rounded</string>
@@ -1944,6 +1945,36 @@ Video playback with AV1 may stutter or drop frames."</string>
<string name="revanced_twitch_debug_title">Debug logging</string>
<string name="revanced_twitch_debug_summary_on">Debug logs are enabled</string>
<string name="revanced_twitch_debug_summary_off">Debug logs are disabled</string>
<!-- Custom settings -->
<string name="revanced_custom_title">Custom</string>
<string name="revanced_custom_reset_title">Reset Custom Settings</string>
<string name="revanced_custom_reset_summary">Restore custom preferences to defaults</string>
<string name="revanced_custom_force_amoled_title">Force AMOLED</string>
<string name="revanced_custom_force_amoled_summary">Use true black in the YouTube UI</string>
<string name="revanced_custom_pure_black_player_title">Pure Black Player</string>
<string name="revanced_custom_pure_black_player_summary">Make player background pure black</string>
<string name="revanced_custom_disable_ambient_mode_title">Disable Ambient Mode</string>
<string name="revanced_custom_disable_ambient_mode_summary">Prevent Ambient Mode from activating</string>
<string name="revanced_custom_hide_shorts_title">Hide Shorts</string>
<string name="revanced_custom_hide_shorts_summary">Remove Shorts UI entries</string>
<string name="revanced_custom_default_playback_speed_title">Default Playback Speed</string>
<string name="revanced_custom_default_playback_speed_summary">Select default playback speed</string>
<string name="revanced_custom_remember_playback_speed_title">Remember Playback Speed</string>
<string name="revanced_custom_remember_playback_speed_summary">Remember speed across videos</string>
<string name="revanced_custom_default_video_quality_title">Default Video Quality</string>
<string name="revanced_custom_default_video_quality_summary">Select default video quality</string>
<string name="revanced_custom_remember_video_quality_title">Remember Video Quality</string>
<string name="revanced_custom_remember_video_quality_summary">Remember quality across videos</string>
<string name="revanced_custom_force_highest_quality_title">Force Highest Available Quality</string>
<string name="revanced_custom_force_highest_quality_summary">Prefer highest quality option</string>
<string name="revanced_custom_debug_video_info_title">Debug Video Information</string>
<string name="revanced_custom_debug_video_info_summary">Show technical video details</string>
<string name="revanced_custom_app_name_title">Custom App Name</string>
<string name="revanced_custom_app_name_summary">Change the visible application name</string>
<string name="revanced_custom_startup_tab_title">Default Startup Tab</string>
<string name="revanced_custom_startup_tab_summary">Select startup tab</string>
<string name="revanced_custom_clean_urls_title">Clean YouTube URLs</string>
<string name="revanced_custom_clean_urls_summary">Remove tracking parameters from shared links</string>
</patch>
</app>
</resources>