fix(YouTube - Video quality): Initial video quality is not overridden

Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com>
This commit is contained in:
oSumAtrIX 2026-03-19 20:41:02 +01:00
parent 009cf71462
commit 53318c48ee
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
5 changed files with 141 additions and 16 deletions

View file

@ -1,5 +1,6 @@
package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.CompositeMatch
import app.revanced.patcher.accessFlags
import app.revanced.patcher.afterAtMost
import app.revanced.patcher.allOf
@ -7,6 +8,7 @@ import app.revanced.patcher.composingFirstMethod
import app.revanced.patcher.custom
import app.revanced.patcher.definingClass
import app.revanced.patcher.field
import app.revanced.patcher.firstMethodComposite
import app.revanced.patcher.firstMethodDeclaratively
import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively
import app.revanced.patcher.gettingFirstMethodDeclaratively
@ -18,6 +20,7 @@ import app.revanced.patcher.opcodes
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.returnType
import app.revanced.util.findFieldFromToString
import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@ -60,6 +63,31 @@ internal val BytecodePatchContext.hidePremiumVideoQualityGetArrayMethod by getti
custom { AccessFlags.SYNTHETIC.isSet(immutableClassDef.accessFlags) }
}
internal const val FIXED_RESOLUTION_STRING = ", initialPlaybackVideoQualityFixedResolution="
internal fun BytecodePatchContext.getPlaybackStartParametersConstructorMethod(): CompositeMatch {
val playbackStartParametersToStringMethod = firstMethodDeclaratively(
FIXED_RESOLUTION_STRING
) {
name("toString")
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("Ljava/lang/String;")
parameterTypes()
}
val initialResolutionField = playbackStartParametersToStringMethod
.findFieldFromToString(FIXED_RESOLUTION_STRING)
// Inject a call to override initial video quality.
return playbackStartParametersToStringMethod.immutableClassDef.firstMethodComposite {
name("<init>")
instructions(
allOf(Opcode.IPUT_OBJECT(), field { this == initialResolutionField })
)
}
}
internal val BytecodePatchContext.videoQualityItemOnClickParentMethod by gettingFirstImmutableMethodDeclaratively(
"VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT",
) {

View file

@ -1,8 +1,15 @@
package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.allOf
import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.field
import app.revanced.patcher.firstMethodComposite
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.name
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
@ -15,6 +22,8 @@ import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.videoQualityChangedMethodMatch
import app.revanced.patches.youtube.video.information.onCreateHook
import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.util.findFieldFromToString
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
private const val EXTENSION_CLASS_DESCRIPTOR =
@ -64,11 +73,28 @@ val rememberVideoQualityPatch = bytecodePatch {
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted")
// Inject a call to override initial video quality.
getPlaybackStartParametersConstructorMethod().let {
it.method.apply {
val index = it[-1]
val register = getInstruction<TwoRegisterInstruction>(index).registerA
addInstructions(
index,
"""
invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->getInitialVideoQuality(Lj$/util/Optional;)Lj$/util/Optional;
move-result-object v$register
"""
)
}
}
// Inject a call to remember the selected quality for Shorts.
videoQualityItemOnClickParentMethod.immutableClassDef.getVideoQualityItemOnClickMethod().addInstruction(
0,
"invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V",
)
videoQualityItemOnClickParentMethod.immutableClassDef.getVideoQualityItemOnClickMethod()
.addInstruction(
0,
"invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V",
)
// Inject a call to remember the user selected quality for regular videos.
videoQualityChangedMethodMatch.let { match ->

View file

@ -38,7 +38,7 @@ import kotlin.collections.remove
*
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
private fun Method.findInstructionIndexFromToString(fieldName: String): Int {
private fun Method.findInstructionIndexFromToString(fieldName: String, isField: Boolean) : Int {
val stringIndex = indexOfFirstInstruction {
val reference = getReference<StringReference>()
reference?.string?.contains(fieldName) == true
@ -67,22 +67,55 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int {
// Should never happen.
throw IllegalArgumentException("Could not find StringBuilder append usage in: $this")
}
val fieldUsageRegister = getInstruction<FiveRegisterInstruction>(fieldUsageIndex).registerD
var fieldUsageRegister = getInstruction<FiveRegisterInstruction>(fieldUsageIndex).registerD
// Look backwards up the method to find the instruction that sets the register.
var fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldUsageIndex - 1) {
fieldUsageRegister == writeRegister
}
// If the field is a method call, then adjust from MOVE_RESULT to the method call.
val fieldSetOpcode = getInstruction(fieldSetIndex).opcode
if (fieldSetOpcode == MOVE_RESULT ||
fieldSetOpcode == MOVE_RESULT_WIDE ||
fieldSetOpcode == MOVE_RESULT_OBJECT
) {
fieldSetIndex--
// Some 'toString()' methods, despite using a StringBuilder, convert the value via
// 'Object.toString()' or 'String.valueOf(object)' before appending it to the StringBuilder.
// In this case, the correct index cannot be found.
// Additional validation is done to find the index of the correct field or method.
//
// Check up to 3 method calls.
var checksLeft = 3
while (checksLeft > 0) {
// If the field is a method call, then adjust from MOVE_RESULT to the method call.
val fieldSetOpcode = getInstruction(fieldSetIndex).opcode
if (fieldSetOpcode == MOVE_RESULT ||
fieldSetOpcode == MOVE_RESULT_WIDE ||
fieldSetOpcode == MOVE_RESULT_OBJECT
) {
fieldSetIndex--
}
val fieldSetReference = getInstruction<ReferenceInstruction>(fieldSetIndex).reference
if (isField && fieldSetReference is FieldReference ||
!isField && fieldSetReference is MethodReference
) {
// Valid index.
return fieldSetIndex
} else if (fieldSetReference is MethodReference &&
// Object.toString(), String.valueOf(object)
fieldSetReference.returnType == "Ljava/lang/String;"
) {
fieldUsageRegister = getInstruction<FiveRegisterInstruction>(fieldSetIndex).registerC
// Look backwards up the method to find the instruction that sets the register.
fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldSetIndex - 1) {
fieldUsageRegister == writeRegister
}
checksLeft--
} else {
throw IllegalArgumentException("Unknown reference: $fieldSetReference")
}
}
return fieldSetIndex
}
@ -93,7 +126,7 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int {
*/
context(context: BytecodePatchContext)
internal fun Method.findMethodFromToString(fieldName: String): MutableMethod {
val methodUsageIndex = findInstructionIndexFromToString(fieldName)
val methodUsageIndex = findInstructionIndexFromToString(fieldName, false)
return context.navigate(this).to(methodUsageIndex).stop()
}
@ -103,7 +136,7 @@ internal fun Method.findMethodFromToString(fieldName: String): MutableMethod {
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
internal fun Method.findFieldFromToString(fieldName: String): FieldReference {
val methodUsageIndex = findInstructionIndexFromToString(fieldName)
val methodUsageIndex = findInstructionIndexFromToString(fieldName, true)
return getInstruction<ReferenceInstruction>(methodUsageIndex).getReference<FieldReference>()!!
}