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

@ -11,8 +11,9 @@ import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.VideoInformation.*; import app.revanced.extension.youtube.patches.VideoInformation.*;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState; import app.revanced.extension.youtube.shared.ShortsPlayerState;
import j$.util.Optional;
@SuppressWarnings("unused") @SuppressWarnings({"rawtypes", "unused"})
public class RememberVideoQualityPatch { public class RememberVideoQualityPatch {
private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI; private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
@ -66,6 +67,25 @@ public class RememberVideoQualityPatch {
} }
} }
/**
* Injection point.
* <p>
* Overrides the initial video quality to not follow the 'Video quality preferences' in YouTube settings.
* (e.g. 'Auto (recommended)' - 360p/480p, 'Higher picture quality' - 720p/1080p...)
* If the maximum video quality available is 1080p and the default video quality is 2160p,
* 1080p is used as an initial video quality.
* <p>
* Called before {@link #newVideoStarted(VideoInformation.PlaybackController)}.
*/
public static Optional getInitialVideoQuality(Optional optional) {
int preferredQuality = getDefaultQualityResolution();
if (preferredQuality != VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) {
Logger.printDebug(() -> "initialVideoQuality: " + preferredQuality);
return Optional.of(preferredQuality);
}
return optional;
}
/** /**
* Injection point. * Injection point.
* @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}.

View file

@ -0,0 +1,18 @@
package j$.util;
public final class Optional<T> {
/**
* Returns an {@code Optional} describing the given non-{@code null}
* value.
*
* @param value the value to describe, which must be non-{@code null}
* @param <T> the type of the value
* @return an {@code Optional} with the value present
* @throws NullPointerException if value is {@code null}
*/
public static <T> Optional<T> of(T value) {
return null;
}
}

View file

@ -1,5 +1,6 @@
package app.revanced.patches.youtube.video.quality package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.CompositeMatch
import app.revanced.patcher.accessFlags import app.revanced.patcher.accessFlags
import app.revanced.patcher.afterAtMost import app.revanced.patcher.afterAtMost
import app.revanced.patcher.allOf import app.revanced.patcher.allOf
@ -7,6 +8,7 @@ import app.revanced.patcher.composingFirstMethod
import app.revanced.patcher.custom import app.revanced.patcher.custom
import app.revanced.patcher.definingClass import app.revanced.patcher.definingClass
import app.revanced.patcher.field import app.revanced.patcher.field
import app.revanced.patcher.firstMethodComposite
import app.revanced.patcher.firstMethodDeclaratively import app.revanced.patcher.firstMethodDeclaratively
import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively
import app.revanced.patcher.gettingFirstMethodDeclaratively import app.revanced.patcher.gettingFirstMethodDeclaratively
@ -18,6 +20,7 @@ import app.revanced.patcher.opcodes
import app.revanced.patcher.parameterTypes import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.returnType import app.revanced.patcher.returnType
import app.revanced.util.findFieldFromToString
import app.revanced.util.literal import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@ -60,6 +63,31 @@ internal val BytecodePatchContext.hidePremiumVideoQualityGetArrayMethod by getti
custom { AccessFlags.SYNTHETIC.isSet(immutableClassDef.accessFlags) } 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( internal val BytecodePatchContext.videoQualityItemOnClickParentMethod by gettingFirstImmutableMethodDeclaratively(
"VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT", "VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT",
) { ) {

View file

@ -1,8 +1,15 @@
package app.revanced.patches.youtube.video.quality package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.allOf
import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.getInstruction 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.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.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch 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.shared.videoQualityChangedMethodMatch
import app.revanced.patches.youtube.video.information.onCreateHook import app.revanced.patches.youtube.video.information.onCreateHook
import app.revanced.patches.youtube.video.information.videoInformationPatch 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 import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
private const val EXTENSION_CLASS_DESCRIPTOR = private const val EXTENSION_CLASS_DESCRIPTOR =
@ -64,11 +73,28 @@ val rememberVideoQualityPatch = bytecodePatch {
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted") 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. // Inject a call to remember the selected quality for Shorts.
videoQualityItemOnClickParentMethod.immutableClassDef.getVideoQualityItemOnClickMethod().addInstruction( videoQualityItemOnClickParentMethod.immutableClassDef.getVideoQualityItemOnClickMethod()
0, .addInstruction(
"invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V", 0,
) "invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V",
)
// Inject a call to remember the user selected quality for regular videos. // Inject a call to remember the user selected quality for regular videos.
videoQualityChangedMethodMatch.let { match -> 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. * @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 stringIndex = indexOfFirstInstruction {
val reference = getReference<StringReference>() val reference = getReference<StringReference>()
reference?.string?.contains(fieldName) == true reference?.string?.contains(fieldName) == true
@ -67,22 +67,55 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int {
// Should never happen. // Should never happen.
throw IllegalArgumentException("Could not find StringBuilder append usage in: $this") 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. // Look backwards up the method to find the instruction that sets the register.
var fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldUsageIndex - 1) { var fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldUsageIndex - 1) {
fieldUsageRegister == writeRegister fieldUsageRegister == writeRegister
} }
// If the field is a method call, then adjust from MOVE_RESULT to the method call. // Some 'toString()' methods, despite using a StringBuilder, convert the value via
val fieldSetOpcode = getInstruction(fieldSetIndex).opcode // 'Object.toString()' or 'String.valueOf(object)' before appending it to the StringBuilder.
if (fieldSetOpcode == MOVE_RESULT || // In this case, the correct index cannot be found.
fieldSetOpcode == MOVE_RESULT_WIDE || // Additional validation is done to find the index of the correct field or method.
fieldSetOpcode == MOVE_RESULT_OBJECT //
) { // Check up to 3 method calls.
fieldSetIndex-- 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 return fieldSetIndex
} }
@ -93,7 +126,7 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int {
*/ */
context(context: BytecodePatchContext) context(context: BytecodePatchContext)
internal fun Method.findMethodFromToString(fieldName: String): MutableMethod { internal fun Method.findMethodFromToString(fieldName: String): MutableMethod {
val methodUsageIndex = findInstructionIndexFromToString(fieldName) val methodUsageIndex = findInstructionIndexFromToString(fieldName, false)
return context.navigate(this).to(methodUsageIndex).stop() 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. * @param fieldName The name of the field to find. Partial matches are allowed.
*/ */
internal fun Method.findFieldFromToString(fieldName: String): FieldReference { internal fun Method.findFieldFromToString(fieldName: String): FieldReference {
val methodUsageIndex = findInstructionIndexFromToString(fieldName) val methodUsageIndex = findInstructionIndexFromToString(fieldName, true)
return getInstruction<ReferenceInstruction>(methodUsageIndex).getReference<FieldReference>()!! return getInstruction<ReferenceInstruction>(methodUsageIndex).getReference<FieldReference>()!!
} }