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()!!
}