diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 1cd584d617..8ce3f2a1c0 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -11,8 +11,9 @@ 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.shared.ShortsPlayerState; +import j$.util.Optional; -@SuppressWarnings("unused") +@SuppressWarnings({"rawtypes", "unused"}) public class RememberVideoQualityPatch { private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI; @@ -66,6 +67,25 @@ public class RememberVideoQualityPatch { } } + /** + * Injection point. + *

+ * 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. + *

+ * 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. * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. diff --git a/extensions/youtube/stub/src/main/java/j$/util/Optional.java b/extensions/youtube/stub/src/main/java/j$/util/Optional.java new file mode 100644 index 0000000000..3f2bb9773e --- /dev/null +++ b/extensions/youtube/stub/src/main/java/j$/util/Optional.java @@ -0,0 +1,18 @@ +package j$.util; + +public final class Optional { + + /** + * Returns an {@code Optional} describing the given non-{@code null} + * value. + * + * @param value the value to describe, which must be non-{@code null} + * @param the type of the value + * @return an {@code Optional} with the value present + * @throws NullPointerException if value is {@code null} + */ + public static Optional of(T value) { + return null; + } + +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt index 614fd9c0f3..305b5d2e13 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt @@ -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("") + instructions( + allOf(Opcode.IPUT_OBJECT(), field { this == initialResolutionField }) + ) + } +} + internal val BytecodePatchContext.videoQualityItemOnClickParentMethod by gettingFirstImmutableMethodDeclaratively( "VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT", ) { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 15db089e5f..36bc2b148d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -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(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 -> diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 5694608dfe..4ee01fe84b 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -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() 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(fieldUsageIndex).registerD + + var fieldUsageRegister = getInstruction(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(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(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(methodUsageIndex).getReference()!! }