fix(YouTube - Video quality): Use 1080p enhanced bitrate for Premium users (#5565)

This commit is contained in:
LisoUseInAIKyrios 2025-07-31 14:27:17 -04:00 committed by GitHub
parent 9ccf13b680
commit 1adbd563b2
7 changed files with 256 additions and 101 deletions

View file

@ -5,9 +5,9 @@ import static app.revanced.extension.shared.Utils.NetworkType;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.lang.reflect.Field; import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.Arrays;
import java.util.List; import java.util.List;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
@ -20,6 +20,14 @@ import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class RememberVideoQualityPatch { public class RememberVideoQualityPatch {
/**
* Interface to use obfuscated methods.
*/
public interface VideoQualityMenuInterface {
void patch_setMenuIndexFromQuality(VideoQuality quality);
}
private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2; private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI; private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting videoQualityMobile = Settings.VIDEO_QUALITY_DEFAULT_MOBILE; private static final IntegerSetting videoQualityMobile = Settings.VIDEO_QUALITY_DEFAULT_MOBILE;
@ -30,7 +38,8 @@ public class RememberVideoQualityPatch {
/** /**
* If the user selected a new quality from the flyout menu, * If the user selected a new quality from the flyout menu,
* and {@link Settings#REMEMBER_VIDEO_QUALITY_LAST_SELECTED} is enabled. * and {@link Settings#REMEMBER_VIDEO_QUALITY_LAST_SELECTED}
* or {@link Settings#REMEMBER_SHORTS_QUALITY_LAST_SELECTED} is enabled.
*/ */
private static boolean userChangedDefaultQuality; private static boolean userChangedDefaultQuality;
@ -40,10 +49,10 @@ public class RememberVideoQualityPatch {
private static int userSelectedQualityIndex; private static int userSelectedQualityIndex;
/** /**
* The available qualities of the current video in human readable form: [1080, 720, 480] * The available qualities of the current video.
*/ */
@Nullable @Nullable
private static List<Integer> videoQualities; private static List<VideoQuality> videoQualities;
private static boolean shouldRememberVideoQuality() { private static boolean shouldRememberVideoQuality() {
BooleanSetting preference = ShortsPlayerState.isOpen() ? BooleanSetting preference = ShortsPlayerState.isOpen() ?
@ -52,23 +61,27 @@ public class RememberVideoQualityPatch {
return preference.get(); return preference.get();
} }
private static void changeDefaultQuality(int defaultQuality) { private static void changeDefaultQuality(int qualityResolution) {
final boolean shortPlayerOpen = ShortsPlayerState.isOpen();
String networkTypeMessage; String networkTypeMessage;
boolean useShortsPreference = ShortsPlayerState.isOpen(); IntegerSetting qualitySetting;
if (Utils.getNetworkType() == NetworkType.MOBILE) { if (Utils.getNetworkType() == NetworkType.MOBILE) {
if (useShortsPreference) shortsQualityMobile.save(defaultQuality);
else videoQualityMobile.save(defaultQuality);
networkTypeMessage = str("revanced_remember_video_quality_mobile"); networkTypeMessage = str("revanced_remember_video_quality_mobile");
qualitySetting = shortPlayerOpen ? shortsQualityMobile : videoQualityMobile;
} else { } else {
if (useShortsPreference) shortsQualityWifi.save(defaultQuality);
else videoQualityWifi.save(defaultQuality);
networkTypeMessage = str("revanced_remember_video_quality_wifi"); networkTypeMessage = str("revanced_remember_video_quality_wifi");
qualitySetting = shortPlayerOpen ? shortsQualityWifi : videoQualityWifi;
} }
qualitySetting.save(qualityResolution);
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
Utils.showToastShort(str( Utils.showToastShort(str(
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast", shortPlayerOpen
networkTypeMessage, (defaultQuality + "p") ? "revanced_remember_video_quality_toast_shorts"
)); : "revanced_remember_video_quality_toast",
networkTypeMessage,
(qualityResolution + "p"))
);
} }
/** /**
@ -77,9 +90,11 @@ public class RememberVideoQualityPatch {
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2 * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
* @param originalQualityIndex quality index to use, as chosen by YouTube * @param originalQualityIndex quality index to use, as chosen by YouTube
*/ */
public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) { public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
try { try {
boolean useShortsPreference = ShortsPlayerState.isOpen(); Utils.verifyOnMainThread();
final boolean useShortsPreference = ShortsPlayerState.isOpen();
final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE
? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get() ? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get()
: (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get(); : (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get();
@ -89,15 +104,7 @@ public class RememberVideoQualityPatch {
} }
if (videoQualities == null || videoQualities.size() != qualities.length) { if (videoQualities == null || videoQualities.size() != qualities.length) {
videoQualities = new ArrayList<>(qualities.length); videoQualities = Arrays.asList(qualities);
for (Object streamQuality : qualities) {
for (Field field : streamQuality.getClass().getFields()) {
if (field.getType().isAssignableFrom(Integer.TYPE)
&& field.getName().length() <= 2) {
videoQualities.add(field.getInt(streamQuality));
}
}
}
// After changing videos the qualities can initially be for the prior video. // After changing videos the qualities can initially be for the prior video.
// So if the qualities have changed an update is needed. // So if the qualities have changed an update is needed.
@ -107,9 +114,9 @@ public class RememberVideoQualityPatch {
if (userChangedDefaultQuality) { if (userChangedDefaultQuality) {
userChangedDefaultQuality = false; userChangedDefaultQuality = false;
final int quality = videoQualities.get(userSelectedQualityIndex); VideoQuality quality = videoQualities.get(userSelectedQualityIndex);
Logger.printDebug(() -> "User changed default quality to: " + quality); Logger.printDebug(() -> "User changed default quality to: " + quality);
changeDefaultQuality(quality); changeDefaultQuality(quality.patch_getResolution());
return userSelectedQualityIndex; return userSelectedQualityIndex;
} }
@ -119,65 +126,86 @@ public class RememberVideoQualityPatch {
qualityNeedsUpdating = false; qualityNeedsUpdating = false;
// Find the highest quality that is equal to or less than the preferred. // Find the highest quality that is equal to or less than the preferred.
int qualityToUse = videoQualities.get(0); // first element is automatic mode VideoQuality qualityToUse = videoQualities.get(0); // First element is automatic mode.
int qualityIndexToUse = 0; int qualityIndexToUse = 0;
int i = 0; int i = 0;
for (Integer quality : videoQualities) { for (VideoQuality quality : videoQualities) {
if (quality <= preferredQuality && qualityToUse < quality) { final int qualityResolution = quality.patch_getResolution();
if (qualityResolution > qualityToUse.patch_getResolution() && qualityResolution <= preferredQuality) {
qualityToUse = quality; qualityToUse = quality;
qualityIndexToUse = i; qualityIndexToUse = i;
break;
} }
i++; i++;
} }
// If the desired quality index is equal to the original index, // If the desired quality index is equal to the original index,
// then the video is already set to the desired default quality. // then the video is already set to the desired default quality.
final int qualityToUseFinal = qualityToUse; String qualityToUseName = qualityToUse.patch_getQualityName();
if (qualityIndexToUse == originalQualityIndex) { if (qualityIndexToUse == originalQualityIndex) {
// On first load of a new video, if the UI video quality flyout menu Logger.printDebug(() -> "Video is already preferred quality: " + qualityToUseName);
// is not updated then it will still show 'Auto' (ie: Auto (480p)),
// even though it's already set to the desired resolution.
//
// To prevent confusion, set the video index anyways (even if it matches the existing index)
// as that will force the UI picker to not display "Auto".
Logger.printDebug(() -> "Video is already preferred quality: " + qualityToUseFinal);
} else { } else {
Logger.printDebug(() -> "Changing video quality from: " Logger.printDebug(() -> "Changing video quality from: "
+ videoQualities.get(originalQualityIndex) + " to: " + qualityToUseFinal); + videoQualities.get(originalQualityIndex).patch_getQualityName()
+ " to: " + qualityToUseName);
} }
Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE); // On first load of a new video, if the video is already the desired quality
m.invoke(qInterface, qualityToUse); // then the quality flyout will show 'Auto' (ie: Auto (720p)).
//
// To prevent user confusion, set the video index even if the
// quality is already correct so the UI picker will not display "Auto".
menu.patch_setMenuIndexFromQuality(qualities[qualityIndexToUse]);
return qualityIndexToUse; return qualityIndexToUse;
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "Failed to set quality", ex); Logger.printException(() -> "setVideoQuality failure", ex);
return originalQualityIndex; return originalQualityIndex;
} }
} }
/** /**
* Injection point. Old quality menu. * Injection point. Fixes bad data used by YouTube.
*/ */
public static void userChangedQuality(int selectedQualityIndex) { public static int fixVideoQualityResolution(String name, int quality) {
final int correctQuality = 480;
if (name.equals("480p") && quality != correctQuality) {
Logger.printDebug(() -> "Fixing bad data of " + name + " from: " + quality
+ " to: " + correctQuality);
return correctQuality;
}
return quality;
}
/**
* Injection point.
* @param qualityIndex Element index of {@link #videoQualities}.
*/
public static void userChangedQuality(int qualityIndex) {
if (shouldRememberVideoQuality()) { if (shouldRememberVideoQuality()) {
userSelectedQualityIndex = selectedQualityIndex; userSelectedQualityIndex = qualityIndex;
userChangedDefaultQuality = true; userChangedDefaultQuality = true;
} }
} }
/** /**
* Injection point. New quality menu. * Injection point.
* @param videoResolution Human readable resolution: 480, 720, 1080.
*/ */
public static void userChangedQualityInNewFlyout(int selectedQuality) { public static void userChangedQualityInFlyout(int videoResolution) {
Utils.verifyOnMainThread();
if (!shouldRememberVideoQuality()) return; if (!shouldRememberVideoQuality()) return;
changeDefaultQuality(selectedQuality); // Quality is human readable resolution (ie: 1080). changeDefaultQuality(videoResolution);
} }
/** /**
* Injection point. * Injection point.
*/ */
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) { public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "newVideoStarted"); Logger.printDebug(() -> "newVideoStarted");
qualityNeedsUpdating = true; qualityNeedsUpdating = true;
videoQualities = null; videoQualities = null;

View file

@ -0,0 +1,12 @@
package com.google.android.libraries.youtube.innertube.model.media;
public class VideoQuality {
public final String patch_getQualityName() {
throw new UnsupportedOperationException("Stub");
}
public final int patch_getResolution() {
throw new UnsupportedOperationException("Stub");
}
}

View file

@ -121,7 +121,7 @@ internal val subtitleButtonControllerFingerprint = fingerprint {
) )
} }
internal val newVideoQualityChangedFingerprint = fingerprint { internal val videoQualityChangedFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("L") returns("L")
parameters("L") parameters("L")

View file

@ -1,7 +1,7 @@
package app.revanced.patches.youtube.video.information package app.revanced.patches.youtube.video.information
import app.revanced.patcher.fingerprint import app.revanced.patcher.fingerprint
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
import app.revanced.util.getReference import app.revanced.util.getReference
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
@ -110,7 +110,7 @@ internal val seekRelativeFingerprint = fingerprint {
} }
/** /**
* Resolves with the class found in [newVideoQualityChangedFingerprint]. * Resolves with the class found in [videoQualityChangedFingerprint].
*/ */
internal val playbackSpeedMenuSpeedChangedFingerprint = fingerprint { internal val playbackSpeedMenuSpeedChangedFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)

View file

@ -9,7 +9,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patcher.util.smali.toInstructions import app.revanced.patcher.util.smali.toInstructions
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
import app.revanced.patches.youtube.video.playerresponse.Hook import app.revanced.patches.youtube.video.playerresponse.Hook
import app.revanced.patches.youtube.video.playerresponse.addPlayerResponseMethodHook import app.revanced.patches.youtube.video.playerresponse.addPlayerResponseMethodHook
import app.revanced.patches.youtube.video.playerresponse.playerResponseMethodHookPatch import app.revanced.patches.youtube.video.playerresponse.playerResponseMethodHookPatch
@ -263,7 +263,7 @@ val videoInformationPatch = bytecodePatch(
// Handle new playback speed menu. // Handle new playback speed menu.
playbackSpeedMenuSpeedChangedFingerprint.match( playbackSpeedMenuSpeedChangedFingerprint.match(
newVideoQualityChangedFingerprint.originalClassDef, videoQualityChangedFingerprint.originalClassDef,
).method.apply { ).method.apply {
val index = indexOfFirstInstructionOrThrow(Opcode.IGET) val index = indexOfFirstInstructionOrThrow(Opcode.IGET)

View file

@ -5,6 +5,21 @@ 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
internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;"
internal val videoQualityFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameters(
"I", // Resolution.
"Ljava/lang/String;", // Human readable resolution: "480p", "1080p Premium", etc
"Z",
"L"
)
custom { _, classDef ->
classDef.type == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
}
}
/** /**
* Matches with the class found in [videoQualitySetterFingerprint]. * Matches with the class found in [videoQualitySetterFingerprint].
*/ */
@ -23,6 +38,22 @@ internal val videoQualityItemOnClickParentFingerprint = fingerprint {
strings("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT") strings("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT")
} }
/**
* Resolves to class found in [videoQualityItemOnClickFingerprint].
*/
internal val videoQualityItemOnClickFingerprint = fingerprint {
returns("V")
parameters(
"Landroid/widget/AdapterView;",
"Landroid/view/View;",
"I",
"J"
)
custom { method, _ ->
method.name == "onItemClick"
}
}
internal val videoQualitySetterFingerprint = fingerprint { internal val videoQualitySetterFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V") returns("V")
@ -37,7 +68,6 @@ internal val videoQualitySetterFingerprint = fingerprint {
strings("menu_item_video_quality") strings("menu_item_video_quality")
} }
internal val videoQualityMenuOptionsFingerprint = fingerprint { internal val videoQualityMenuOptionsFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC) accessFlags(AccessFlags.STATIC)
returns("[L") returns("[L")

View file

@ -3,8 +3,8 @@ package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
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
import app.revanced.patches.shared.misc.settings.preference.ListPreference import app.revanced.patches.shared.misc.settings.preference.ListPreference
@ -12,15 +12,21 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
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 com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
private const val EXTENSION_CLASS_DESCRIPTOR = private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch;" "Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch;"
private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE =
"Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch\$VideoQualityMenuInterface;"
val rememberVideoQualityPatch = bytecodePatch { val rememberVideoQualityPatch = bytecodePatch {
dependsOn( dependsOn(
@ -70,72 +76,151 @@ val rememberVideoQualityPatch = bytecodePatch {
*/ */
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted") onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted")
videoQualityFingerprint.let {
// Fix bad data used by YouTube.
it.method.addInstructions(
0,
"""
invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I
move-result p1
"""
)
// Add methods to access obfuscated quality fields.
it.classDef.apply {
methods.add(
ImmutableMethod(
type,
"patch_getQualityName",
listOf(),
"Ljava/lang/String;",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
// Only one string field.
val qualityNameField = fields.single { field ->
field.type == "Ljava/lang/String;"
}
addInstructions(
0,
"""
iget-object v0, p0, $qualityNameField
return-object v0
"""
)
}
)
methods.add(
ImmutableMethod(
type,
"patch_getResolution",
listOf(),
"I",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
val resolutionField = fields.single { field ->
field.type == "I"
}
addInstructions(
0,
"""
iget v0, p0, $resolutionField
return v0
"""
)
}
)
}
}
// Inject a call to set the remembered quality once a video loads. // Inject a call to set the remembered quality once a video loads.
setQualityByIndexMethodClassFieldReferenceFingerprint.match( setQualityByIndexMethodClassFieldReferenceFingerprint.match(
videoQualitySetterFingerprint.originalClassDef, videoQualitySetterFingerprint.originalClassDef
).let { match -> ).let { match ->
// This instruction refers to the field with the type that contains the setQualityByIndex method. // This instruction refers to the field with the type that contains the setQualityByIndex method.
val instructions = match.method.implementation!!.instructions val instructions = match.method.implementation!!.instructions
val onItemClickListenerClassReference =
val getOnItemClickListenerClassReference =
(instructions.elementAt(0) as ReferenceInstruction).reference (instructions.elementAt(0) as ReferenceInstruction).reference
val getSetQualityByIndexMethodClassFieldReference = val setQualityFieldReference =
(instructions.elementAt(1) as ReferenceInstruction).reference ((instructions.elementAt(1) as ReferenceInstruction).reference) as FieldReference
val setQualityByIndexMethodClassFieldReference = proxy(
getSetQualityByIndexMethodClassFieldReference as FieldReference classes.find { classDef ->
classDef.type == setQualityFieldReference.type
val setQualityByIndexMethodClass = classes }!!
.find { classDef -> classDef.type == setQualityByIndexMethodClassFieldReference.type }!! ).mutableClass.apply {
// Add interface and helper methods to allow extension code to call obfuscated methods.
interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE)
// Get the name of the setQualityByIndex method. // Get the name of the setQualityByIndex method.
val setQualityByIndexMethod = setQualityByIndexMethodClass.methods val setQualityMenuIndexMethod = methods.single {
.find { method -> method.parameterTypes.first() == "I" } method -> method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
?: throw PatchException("Could not find setQualityByIndex method") }
methods.add(
ImmutableMethod(
type,
"patch_setMenuIndexFromQuality",
listOf(
ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null)
),
"V",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
addInstructions(
0,
"""
invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod
return-void
"""
)
}
)
}
videoQualitySetterFingerprint.method.addInstructions( videoQualitySetterFingerprint.method.addInstructions(
0, 0,
""" """
# Get the object instance to invoke the setQualityByIndex method on. # Get the object instance to invoke the setQualityByIndex method on.
iget-object v0, p0, $getOnItemClickListenerClassReference iget-object v0, p0, $onItemClickListenerClassReference
iget-object v0, v0, $getSetQualityByIndexMethodClassFieldReference iget-object v0, v0, $setQualityFieldReference
# Get the method name. invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I
const-string v1, "${setQualityByIndexMethod.name}"
# Set the quality.
# The first parameter is the array list of video qualities.
# The second parameter is the index of the selected quality.
# The register v0 stores the object instance to invoke the setQualityByIndex method on.
# The register v1 stores the name of the setQualityByIndex method.
invoke-static { p1, p2, v0, v1 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/String;)I
move-result p2 move-result p2
""", """
) )
} }
// Inject a call to remember the selected quality. // Inject a call to remember the selected quality.
videoQualityItemOnClickParentFingerprint.classDef.methods.find { it.name == "onItemClick" } videoQualityItemOnClickFingerprint.match(
?.apply { videoQualityItemOnClickParentFingerprint.classDef
val listItemIndexParameter = 3 ).method.addInstruction(
addInstruction(
0, 0,
"invoke-static { p$listItemIndexParameter }, " + "invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedQuality(I)V"
"$EXTENSION_CLASS_DESCRIPTOR->userChangedQuality(I)V",
) )
} ?: throw PatchException("Failed to find onItemClick method")
// Remember video quality if not using old layout menu. // Inject a call to remember the user selected quality.
newVideoQualityChangedFingerprint.method.apply { videoQualityChangedFingerprint.let {
val index = newVideoQualityChangedFingerprint.patternMatch!!.startIndex it.method.apply {
val qualityRegister = getInstruction<TwoRegisterInstruction>(index).registerA val index = it.patternMatch!!.startIndex
val register = getInstruction<TwoRegisterInstruction>(index).registerA
addInstruction( addInstruction(
index + 1, index + 1,
"invoke-static { v$qualityRegister }, " + "invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->userChangedQualityInFlyout(I)V",
"$EXTENSION_CLASS_DESCRIPTOR->userChangedQualityInNewFlyout(I)V",
) )
} }
} }
} }
}