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(); 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. * Injection point.
*/ */
@@ -31,14 +31,11 @@ internal val BytecodePatchContext.buildInitPlaybackRequestMethodMatch by composi
} }
internal val BytecodePatchContext.buildPlayerRequestURIMethodMatch by composingFirstMethod("key", "asig") { internal val BytecodePatchContext.buildPlayerRequestURIMethodMatch by composingFirstMethod("key", "asig") {
returnType("Ljava/lang/String;") returnType($$"Landroid/net/Uri$Builder;")
opcodes( 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.MOVE_RESULT_OBJECT,
Opcode.IPUT_OBJECT, Opcode.RETURN_OBJECT, // Returns the built player request Uri$Builder.
Opcode.IGET_OBJECT,
Opcode.MONITOR_EXIT,
Opcode.RETURN_OBJECT,
) )
} }
@@ -154,7 +151,7 @@ internal val BytecodePatchContext.nerdsStatsVideoFormatBuilderMethod by gettingF
returnType("Ljava/lang/String;") returnType("Ljava/lang/String;")
parameterTypes("L") parameterTypes("L")
instructions( instructions(
"codecs=\""(), ", codecs="(),
) )
} }
@@ -82,14 +82,19 @@ internal fun spoofVideoStreamsPatch(
// region Block /get_watch requests to fall back to /player requests. // region Block /get_watch requests to fall back to /player requests.
buildPlayerRequestURIMethodMatch.method.apply { buildPlayerRequestURIMethodMatch.method.apply {
val invokeToStringIndex = buildPlayerRequestURIMethodMatch[0] val returnIndex = buildPlayerRequestURIMethodMatch[-1]
val uriRegister = getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC val builderRegister = getInstruction<OneRegisterInstruction>(returnIndex).registerA
val freeRegister = findFreeRegister(returnIndex, builderRegister)
addInstructions( addInstructions(
invokeToStringIndex, returnIndex,
""" $$"""
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; invoke-virtual { v$builderRegister }, Landroid/net/Uri$Builder;->build()Landroid/net/Uri;
move-result-object v$uriRegister 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.fix.backtoexitgesture.fixBackToExitGesturePatch
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch 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_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.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
@@ -34,6 +35,7 @@ import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.injectHideViewCall import app.revanced.util.injectHideViewCall
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction 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 import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
private const val EXTENSION_CLASS_DESCRIPTOR = private const val EXTENSION_CLASS_DESCRIPTOR =
@@ -94,7 +96,8 @@ val hideAdsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "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 // Hide player overlay view. This can be hidden with a regular litho filter
// but an empty space remains. // but an empty space remains.
if (is_20_14_or_greater) { if (is_20_14_or_greater) {
playerOverlayTimelyShelfMethod.addInstructionsWithLabels( if (is_21_34_or_greater) {
0, // 21.34+ the overlay handler moved into a synthetic lambda `a(Object)V`
""" // that dispatches on overlay type. Hook right after the `String.equals` /
invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z // `move-result` that determines the overlay is the timely shelf so that
move-result v0 // only the timely shelf is hidden and not every player overlay.
if-eqz v0, :show playerOverlayTimelyShelfMethod21_34.let { match ->
return-void val insertIndex = match[-1]
:show val equalsResultRegister =
nop 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.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch 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_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.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
@@ -68,7 +69,8 @@ val hideEndScreenCardsPatch = bytecodePatch(
"20.26.46", "20.26.46",
"20.31.42", "20.31.42",
"20.37.48", "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( showEndscreenCardsMethod.addInstructionsWithLabels(
0, 0,
""" """
@@ -92,8 +92,15 @@ val miniplayerPatch = bytecodePatch(
val preferences = mutableSetOf<BasePreference>() val preferences = mutableSetOf<BasePreference>()
preferences += preferences +=
if (is_20_37_or_greater) { if (is_20_37_or_greater && !is_21_34_or_greater) {
ListPreference("revanced_miniplayer_type") 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) { } else if (is_20_03_or_greater) {
ListPreference( ListPreference(
key = "revanced_miniplayer_type", 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( PreferenceScreen.PLAYER.addPreferences(
PreferenceScreenPreference( PreferenceScreenPreference(
@@ -265,101 +274,113 @@ val miniplayerPatch = bytecodePatch(
// region Enable modern miniplayer. // region Enable modern miniplayer.
miniplayerModernConstructorMethod.classDef.methods.forEach { // Select which modern miniplayer type is used.
it.apply { // YouTube 21.29+ removed all modern miniplayers except "Modern 4", so there is
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { // nothing to choose between and this override is dead code.
val iPutIndex = indexOfFirstInstructionOrThrow { if (!is_21_34_or_greater) {
this.opcode == Opcode.IPUT && this.getReference<FieldReference>()?.type == "I" 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) insertModernMiniplayerTypeOverride(iPutIndex)
} else { } else {
findReturnIndicesReversed().forEach { index -> findReturnIndicesReversed().forEach { index ->
insertModernMiniplayerOverride( insertModernMiniplayerOverride(
index index
) )
}
} }
} }
} }
} }
if (is_19_23_or_greater) { // Modern miniplayer feature-flag overrides.
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( // YouTube 21.29+ removed all modern miniplayers except "Modern 4" and moved/removed the
MINIPLAYER_DRAG_DROP_FEATURE_KEY, // feature flags for drag & drop, double-tap, rounded corners, horizontal drag and animated
"getMiniplayerDragAndDrop", // 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) {
if (is_19_25_or_greater) { miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( MINIPLAYER_DRAG_DROP_FEATURE_KEY,
MINIPLAYER_MODERN_FEATURE_LEGACY_KEY, "getMiniplayerDragAndDrop",
"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
""",
) )
} }
// Override a minimum size constant. if (is_19_25_or_greater) {
miniplayerMinimumSizeMethodMatch.let { miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
it.method.apply { MINIPLAYER_MODERN_FEATURE_LEGACY_KEY,
val index = it[1] "getModernMiniplayerOverride",
val register = getInstruction<OneRegisterInstruction>(index).registerA )
// Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller. miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
// The 170 initial limit probably could be patched to allow even smaller initial sizes, MINIPLAYER_MODERN_FEATURE_KEY,
// but 170 is already half the horizontal space and smaller does not seem useful. "getModernFeatureFlagsActiveOverride",
replaceInstruction(index, "const/16 v$register, 170") )
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) { if (is_19_36_or_greater) {
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY, MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY,
"getRoundedCorners", "getRoundedCorners",
) )
} }
if (is_19_43_or_greater) { if (is_19_43_or_greater) {
miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerOnCloseHandlerMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_DISABLED_FEATURE_KEY, MINIPLAYER_DISABLED_FEATURE_KEY,
"getMiniplayerOnCloseHandler", "getMiniplayerOnCloseHandler",
) )
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY, MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY,
"getHorizontalDrag", "getHorizontalDrag",
) )
miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride( miniplayerModernConstructorMethod.insertMiniplayerFeatureFlagBooleanOverride(
MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY, MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY,
"getMaximizeAnimation", "getMaximizeAnimation",
) )
}
} }
// endregion // endregion
@@ -391,7 +412,8 @@ val miniplayerPatch = bytecodePatch(
// region fix minimal miniplayer using the wrong pause/play bold icons. // 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 { miniplayerSetIconsMethod.apply {
findInstructionIndicesReversedOrThrow { findInstructionIndicesReversedOrThrow {
val reference = getReference<MethodReference>() val reference = getReference<MethodReference>()
@@ -421,7 +443,6 @@ val miniplayerPatch = bytecodePatch(
ClassDef::miniplayerModernActionButtonMethodMatch to "hideMiniplayerActionButton", ClassDef::miniplayerModernActionButtonMethodMatch to "hideMiniplayerActionButton",
ClassDef::miniplayerModernRewindButtonMethodMatch to "hideMiniplayerRewindForward", ClassDef::miniplayerModernRewindButtonMethodMatch to "hideMiniplayerRewindForward",
ClassDef::miniplayerModernForwardButtonMethodMatch to "hideMiniplayerRewindForward", ClassDef::miniplayerModernForwardButtonMethodMatch to "hideMiniplayerRewindForward",
ClassDef::miniplayerModernOverlayViewMethodMatch to "adjustMiniplayerOpacity",
).forEach { (matching, methodName) -> ).forEach { (matching, methodName) ->
val match = matching.get(miniplayerModernViewParentMethod.immutableClassDef) 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() miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernAddViewListenerMethod()
.addInstruction( .addInstruction(
0, 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.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater 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_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.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
@@ -93,103 +94,109 @@ val shortsAutoplayPatch = bytecodePatch(
) )
} }
val reelPlaybackRepeatMethod = // YouTube 21.29+ replaced the diagnostics string and reworked the reel playback repeat
reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod() // 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
reelPlaybackRepeatMethod.apply { // versions below 21.34.
// The behavior enums are looked up from an ordinal value to an enum type. if (!is_21_34_or_greater) {
findInstructionIndicesReversedOrThrow { val reelPlaybackRepeatMethod =
val reference = getReference<MethodReference>() reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod()
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!!
reelPlaybackRepeatMethod.apply { reelPlaybackRepeatMethod.apply {
// Find the first call modified by extension code above. // The behavior enums are looked up from an ordinal value to an enum type.
val extensionReturnResultIndex = indexOfFirstInstructionOrThrow { findInstructionIndicesReversedOrThrow {
opcode == Opcode.INVOKE_STATIC && val reference = getReference<MethodReference>()
getReference<MethodReference>()?.definingClass == EXTENSION_CLASS_DESCRIPTOR reference?.definingClass == reelEnumClass &&
} + 1 reference.parameterTypes.firstOrNull() == "I" &&
val enumRegister = reference.returnType == reelEnumClass
getInstruction<OneRegisterInstruction>(extensionReturnResultIndex).registerA }.forEach { index ->
val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow { val register = getInstruction<OneRegisterInstruction>(index + 1).registerA
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. addInstructions(
// If enum is autoplay then method performs autoplay and returns null, index + 2,
// 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 invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum;
move-result v0 move-result-object v$register
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( // As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method.
extensionReturnResultIndex + 1, // Manually restore the removed 'Autoplay' code.
""" if (is_20_09_or_greater) {
invoke-direct { p0, v$enumRegister }, $helperClass->$helperName(Ljava/lang/Enum;)$helperReturnType // Variable names are only a rough guess of what these methods do.
move-result-object v$enumRegister val userActionMethodReference =
if-nez v$enumRegister, :ignore reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[1]).methodReference!!
return-void # Autoplay was performed. val reelSequenceControllerMethodReference =
:ignore reelPlaybackMethodMatch.method.getInstruction(reelPlaybackMethodMatch[2]).methodReference!!
nop
""", 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.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater 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_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.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
@@ -103,7 +104,10 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch(
kidsBackgroundPlaybackPolicyControllerMethod.returnEarly() kidsBackgroundPlaybackPolicyControllerMethod.returnEarly()
// Fix PiP buttons not working after locking/unlocking device screen. // 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 { pipInputConsumerFeatureFlagMethodMatch.let {
it.method.insertLiteralOverride( it.method.insertLiteralOverride(
it[0], it[0],
@@ -15,6 +15,7 @@ import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.returnType import app.revanced.patcher.returnType
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch 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.addInstructionsAtControlFlowLabel
import app.revanced.util.cloneMutableAndPreserveParameters import app.revanced.util.cloneMutableAndPreserveParameters
import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.findInstructionIndicesReversedOrThrow
@@ -73,12 +74,25 @@ val hookClientContextPatch = bytecodePatch(
} }
} }
authenticationChangeListenerMethod.apply { // The Authentication changed listener method (where the message lite builder method was
val messageLiteBuilderIndex = // sourced from) was removed in YouTube 21.34+. Derive the builder method from the dummy
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass) // 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 = messageLiteBuilderMethod =
getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!! getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!!
}
} else {
buildDummyClientContextBodyMethodMatch.method.apply {
val messageLiteBuilderIndex =
indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass)
messageLiteBuilderMethod =
getInstruction<ReferenceInstruction>(messageLiteBuilderIndex).methodReference!!
}
} }
buildClientContextBodyConstructorMethod.immutableClassDef.buildClientContextBodyMethodMatch.let { buildClientContextBodyConstructorMethod.immutableClassDef.buildClientContextBodyMethodMatch.let {
@@ -11,7 +11,6 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch
returnType("V") returnType("V")
parameterTypes() parameterTypes()
instructions( instructions(
ResourceType.DRAWABLE("ic_offline_no_content_upside_down"),
ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"), ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"),
method { name == "getString" && returnType == "Ljava/lang/String;" }, method { name == "getString" && returnType == "Ljava/lang/String;" },
after(Opcode.MOVE_RESULT_OBJECT()), 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, // 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. // 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 { internal val BytecodePatchContext.loadingFrameLayoutControllerMethodMatch by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V") returnType("V")
parameterTypes("L") parameterTypes("Z")
instructions( instructions(
ResourceType.DRAWABLE("ic_offline_no_content_upside_down"),
ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"), ResourceType.STRING("offline_no_content_body_text_not_offline_eligible"),
method { name == "getString" && returnType == "Ljava/lang/String;" }, method { name == "getString" && returnType == "Ljava/lang/String;" },
after(Opcode.MOVE_RESULT_OBJECT()), 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). // Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf).
lithoConverterBufferUpbFeatureFlagMethodMatch.let { // The flag (45419603) was removed in 21.34. In 21.34 the native UPB buffer path is the
// 20.22 the flag is still enabled in one location, but what it does is not known. // default (and required) path for the setProtoBuffer([B) hook, so nothing needs to be
// Disable it anyway. // forced off.
it.method.insertLiteralOverride( if (!is_21_34_or_greater) {
it[0], lithoConverterBufferUpbFeatureFlagMethodMatch.let {
false, // 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 // endregion
} }
@@ -34,6 +34,27 @@ internal val ClassDef.motionEventMethodMatch by ClassDefComposing.composingFirst
instructions(method("setTranslationY")) 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 { internal val BytecodePatchContext.playerControlsExtensionHookListenersExistMethod by gettingFirstMethodDeclaratively {
name("fullscreenButtonVisibilityCallbacksExist") name("fullscreenButtonVisibilityCallbacksExist")
definingClass(EXTENSION_CLASS_DESCRIPTOR) definingClass(EXTENSION_CLASS_DESCRIPTOR)
@@ -268,7 +268,13 @@ val playerControlsPatch = bytecodePatch(
visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistMethod visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistMethod
visibilityImmediateMethod = playerControlsExtensionHookMethod 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 visibilityNegatedImmediateMethod = it.method
visibilityNegatedImmediateInsertIndex = it[0] + 1 visibilityNegatedImmediateInsertIndex = it[0] + 1
} }
@@ -127,6 +127,8 @@ var is_21_07_or_greater : Boolean by Delegates.notNull()
private set private set
var is_21_08_or_greater : Boolean by Delegates.notNull() var is_21_08_or_greater : Boolean by Delegates.notNull()
private set private set
var is_21_34_or_greater: Boolean by Delegates.notNull()
private set
val versionCheckPatch = resourcePatch( val versionCheckPatch = resourcePatch(
description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.", 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_06_or_greater = 260705000 <= playStoreServicesVersion
is_21_07_or_greater = 260805000 <= playStoreServicesVersion is_21_07_or_greater = 260805000 <= playStoreServicesVersion
is_21_08_or_greater = 260905000 <= 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")) 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 { internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFirstMethod {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("Z") returnType("Z")
@@ -29,3 +50,22 @@ internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFir
afterAtMost(10, Opcode.MOVE_RESULT()), 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.fix.playbackspeed.fixPlaybackSpeedWhilePlayingPatch
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater 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_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.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.util.* import app.revanced.util.*
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
@@ -214,6 +215,64 @@ val settingsPatch = bytecodePatch(
) )
PreferenceScreen.MISC.addPreferences( 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( TextPreference(
key = null, key = null,
titleKey = "revanced_pref_import_export_title", titleKey = "revanced_pref_import_export_title",
@@ -230,7 +289,7 @@ val settingsPatch = bytecodePatch(
// Update shared dark mode status based on YT theme. // Update shared dark mode status based on YT theme.
// This is needed because YT allows forcing light/dark mode // This is needed because YT allows forcing light/dark mode
// which then differs from the system dark mode status. // 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 -> findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
val register = getInstruction<OneRegisterInstruction>(index).registerA val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructionsAtControlFlowLabel( addInstructionsAtControlFlowLabel(
@@ -241,10 +300,12 @@ val settingsPatch = bytecodePatch(
} }
// Add setting to force Cairo settings fragment on/off. // Add setting to force Cairo settings fragment on/off.
cairoFragmentConfigMethodMatch.method.insertLiteralOverride( (if (is_21_34_or_greater) cairoFragmentConfigMethodMatch21_34 else cairoFragmentConfigMethodMatch).let {
cairoFragmentConfigMethodMatch[0], it.method.insertLiteralOverride(
"$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z", 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. // 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. // 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) 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. * 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_5">Modern 2</string>
<string name="revanced_miniplayer_type_entry_6">Modern 3</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_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_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_on">Corners are square</string>
<string name="revanced_miniplayer_disable_rounded_corners_summary_off">Corners are rounded</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_title">Debug logging</string>
<string name="revanced_twitch_debug_summary_on">Debug logs are enabled</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> <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> </patch>
</app> </app>
</resources> </resources>