1+ package io.github.chsbuffer.revancedxposed.youtube.video
2+
3+ import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory
4+ import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch
5+ import de.robv.android.xposed.XC_MethodHook
6+ import io.github.chsbuffer.revancedxposed.AccessFlags
7+ import io.github.chsbuffer.revancedxposed.Opcode
8+ import io.github.chsbuffer.revancedxposed.ScopedHook
9+ import io.github.chsbuffer.revancedxposed.fingerprint
10+ import io.github.chsbuffer.revancedxposed.getIntField
11+ import io.github.chsbuffer.revancedxposed.shared.misc.settings.preference.ListPreference
12+ import io.github.chsbuffer.revancedxposed.shared.misc.settings.preference.PreferenceCategory
13+ import io.github.chsbuffer.revancedxposed.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
14+ import io.github.chsbuffer.revancedxposed.shared.misc.settings.preference.SwitchPreference
15+ import io.github.chsbuffer.revancedxposed.youtube.YoutubeHook
16+ import io.github.chsbuffer.revancedxposed.youtube.misc.PreferenceScreen
17+
18+ fun YoutubeHook.RememberVideoQuality () {
19+ val settingsMenuVideoQualityGroup = setOf (
20+ ListPreference (
21+ key = " revanced_video_quality_default_mobile" ,
22+ entriesKey = " revanced_video_quality_default_entries" ,
23+ entryValuesKey = " revanced_video_quality_default_entry_values"
24+ ),
25+ ListPreference (
26+ key = " revanced_video_quality_default_wifi" ,
27+ entriesKey = " revanced_video_quality_default_entries" ,
28+ entryValuesKey = " revanced_video_quality_default_entry_values"
29+ ),
30+ SwitchPreference (" revanced_remember_video_quality_last_selected" ),
31+
32+ ListPreference (
33+ key = " revanced_shorts_quality_default_mobile" ,
34+ entriesKey = " revanced_shorts_quality_default_entries" ,
35+ entryValuesKey = " revanced_shorts_quality_default_entry_values" ,
36+ ),
37+ ListPreference (
38+ key = " revanced_shorts_quality_default_wifi" ,
39+ entriesKey = " revanced_shorts_quality_default_entries" ,
40+ entryValuesKey = " revanced_shorts_quality_default_entry_values"
41+ ),
42+ SwitchPreference (" revanced_remember_shorts_quality_last_selected" ),
43+ SwitchPreference (" revanced_remember_video_quality_last_selected_toast" )
44+ )
45+
46+ PreferenceScreen .VIDEO .addPreferences(
47+ // Keep the preferences organized together.
48+ PreferenceCategory (
49+ key = " revanced_01_video_key" , // Dummy key to force the quality preferences first.
50+ titleKey = null ,
51+ sorting = Sorting .UNSORTED ,
52+ tag = NoTitlePreferenceCategory ::class .java,
53+ preferences = settingsMenuVideoQualityGroup
54+ )
55+ )
56+
57+ /*
58+ * The following code works by hooking the method which is called when the user selects a video quality
59+ * to remember the last selected video quality.
60+ *
61+ * It also hooks the method which is called when the video quality to set is determined.
62+ * Conveniently, at this point the video quality is overridden to the remembered playback speed.
63+ */
64+ playerInitHooks.add { controller ->
65+ RememberVideoQualityPatch .newVideoStarted(controller)
66+ }
67+
68+ val videoQualitySetterFingerprint = getDexMethod(" videoQualitySetterFingerprint" ) {
69+ fingerprint {
70+ accessFlags(AccessFlags .PUBLIC , AccessFlags .FINAL )
71+ returns(" V" )
72+ parameters(" [L" , " I" , " Z" )
73+ opcodes(
74+ Opcode .IF_EQZ ,
75+ Opcode .INVOKE_VIRTUAL ,
76+ Opcode .MOVE_RESULT_OBJECT ,
77+ Opcode .INVOKE_VIRTUAL ,
78+ Opcode .IPUT_BOOLEAN ,
79+ )
80+ strings(" menu_item_video_quality" )
81+ }
82+ }
83+
84+ getDexMethod(" setQualityByIndexMethodClassFieldReferenceFingerprint" ) {
85+ fingerprint {
86+ returns(" V" )
87+ parameters(" L" )
88+ opcodes(
89+ Opcode .IGET_OBJECT ,
90+ Opcode .IPUT_OBJECT ,
91+ Opcode .IGET_OBJECT ,
92+ )
93+ classMatcher {
94+ className = videoQualitySetterFingerprint.className
95+ }
96+ }.also { method ->
97+ val usingFields = method.usingFields
98+ getDexField(" getOnItemClickListenerClassReference" ) {
99+ usingFields[0 ].field
100+ }
101+ getDexField(" getSetQualityByIndexMethodClassFieldReference" ) {
102+ usingFields[1 ].field
103+ }
104+ getDexMethod(" setQualityByIndexMethod" ) {
105+ usingFields[1 ].field.type.findMethod { matcher { paramTypes(" int" ) } }.single()
106+ }
107+ }
108+ }
109+
110+ // Inject a call to set the remembered quality once a video loads.
111+ videoQualitySetterFingerprint.hookMethod(object : XC_MethodHook () {
112+ val getOnItemClickListener = getDexField(" getOnItemClickListenerClassReference" ).toField()
113+ val getSetQualityByIndexMethod =
114+ getDexField(" getSetQualityByIndexMethodClassFieldReference" ).toField()
115+ val setQualityByIndexMethod = getDexMethod(" setQualityByIndexMethod" ).name
116+
117+ @Suppress(" UNCHECKED_CAST" )
118+ override fun beforeHookedMethod (param : MethodHookParam ) {
119+ val qualities = param.args[0 ] as Array <out Any >
120+ val originalQualityIndex = param.args[1 ] as Int
121+ val qInterface = param.thisObject.let { getOnItemClickListener.get(it) }
122+ .let { getSetQualityByIndexMethod.get(it) }
123+ val qIndexMethod = setQualityByIndexMethod
124+ param.args[1 ] = RememberVideoQualityPatch .setVideoQuality(
125+ qualities, originalQualityIndex, qInterface, qIndexMethod
126+ )
127+ }
128+ })
129+
130+
131+ // Inject a call to remember the selected quality.
132+ getDexMethod(" videoQualityItemOnClickParentFingerprint" ) {
133+ fingerprint {
134+ returns(" V" )
135+ strings(" VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT" )
136+ }.declaredClass!! .findMethod {
137+ matcher {
138+ name = " onItemClick"
139+ }
140+ }.single()
141+ }.hookMethod(object : XC_MethodHook () {
142+ override fun beforeHookedMethod (param : MethodHookParam ) {
143+ RememberVideoQualityPatch .userChangedQuality(param.args[2 ] as Int )
144+ }
145+ })
146+
147+ // Remember video quality if not using old layout menu.
148+ getDexMethod(" newVideoQualityChangedFingerprint" ) {
149+ fingerprint {
150+ accessFlags(AccessFlags .PUBLIC , AccessFlags .FINAL )
151+ returns(" L" )
152+ parameters(" L" )
153+ methodMatcher {
154+ opNames(
155+ listOf (
156+ " iget" ,
157+ " const/4" ,
158+ " if-ne" ,
159+ " new-instance" ,
160+ " iget-object" ,
161+ " check-cast" ,
162+ " iget"
163+ )
164+ )
165+ addInvoke {
166+ declaredClass =
167+ " com.google.android.libraries.youtube.innertube.model.media.VideoQuality"
168+ name = " <init>"
169+ }
170+ }
171+ }.also { method ->
172+ getDexMethod(" VideoQualityReceiver" ) {
173+ method.invokes.single { it.paramCount == 1 && it.paramTypeNames[0 ] == " com.google.android.libraries.youtube.innertube.model.media.VideoQuality" }
174+ }
175+ }
176+ }.hookMethod(ScopedHook (getDexMethod(" VideoQualityReceiver" ).toMember()) {
177+ before {
178+ val selectedQualityIndex = param.args[0 ].getIntField(" a" )
179+ RememberVideoQualityPatch .userChangedQualityInNewFlyout(selectedQualityIndex)
180+ }
181+ })
182+ }
0 commit comments