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:
parent
009cf71462
commit
53318c48ee
5 changed files with 141 additions and 16 deletions
|
|
@ -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()}.
|
||||||
|
|
|
||||||
18
extensions/youtube/stub/src/main/java/j$/util/Optional.java
Normal file
18
extensions/youtube/stub/src/main/java/j$/util/Optional.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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>()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue